Compare commits

...

34 Commits

Author SHA1 Message Date
Sean Goedecke c37f296c98 Merge pull request #84 from actions/sgoedecke/better-error-logging
Log specific error even if it is not an Error
2025-08-05 09:28:24 +10:00
Sean Goedecke e7ddc840ba npm run package 2025-08-04 23:00:34 +00:00
Sean Goedecke fa321d4c78 Update src/main.ts
Co-authored-by: Marais Rossouw <me@marais.co>
2025-08-05 08:59:43 +10:00
Sean Goedecke 3b5da63917 update tests 2025-08-04 22:44:17 +00:00
Sean Goedecke a620b9fa98 Force exit on error 2025-08-04 22:40:30 +00:00
Sean Goedecke a6d2a86ab3 Log specific error even if it is not an Error 2025-08-04 22:28:10 +00:00
Sean Goedecke 4b591cc529 Merge pull request #83 from actions/sgoedecke/separate-mcp
Separate out MCP token
2025-08-04 15:03:54 +10:00
Sean Goedecke ea24ec2ed4 Update README.md
Co-authored-by: Yuzuki <36879321+Yuzuki-S@users.noreply.github.com>
2025-08-04 13:52:21 +10:00
Sean Goedecke b9f9444fb7 update docs 2025-08-04 03:41:34 +00:00
Sean Goedecke 419f171f16 Separate out MCP token 2025-08-04 03:06:53 +00:00
Yumin Wong fc8527d1d9 Merge pull request #74 from actions/dependabot/github_actions/actions-minor-e893b3f303
chore(deps): bump actions/publish-action from 0.2.2 to 0.3.0 in the actions-minor group
2025-07-29 14:00:29 +08:00
Yumin Wong 719349dfcc Merge branch 'main' into dependabot/github_actions/actions-minor-e893b3f303 2025-07-29 13:30:19 +08:00
Yumin Wong 2762750922 Merge pull request #76 from actions/dependabot/npm_and_yarn/rollup/rollup-linux-x64-gnu-4.46.0
chore(deps): bump @rollup/rollup-linux-x64-gnu from 4.45.1 to 4.46.0
2025-07-29 13:20:25 +08:00
dependabot[bot] 9386906af5 chore(deps): bump @rollup/rollup-linux-x64-gnu from 4.45.1 to 4.46.0
Bumps [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup) from 4.45.1 to 4.46.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.45.1...v4.46.0)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 03:48:49 +00:00
dependabot[bot] ca9eff7051 chore(deps): bump actions/publish-action in the actions-minor group
Bumps the actions-minor group with 1 update: [actions/publish-action](https://github.com/actions/publish-action).


Updates `actions/publish-action` from 0.2.2 to 0.3.0
- [Commits](https://github.com/actions/publish-action/compare/v0.2.2...v0.3.0)

---
updated-dependencies:
- dependency-name: actions/publish-action
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 03:05:06 +00:00
Marais Rossouw 6bef1d0031 Merge pull request #72 from actions/mr/linters 2025-07-24 19:50:25 +10:00
Marais Rossouw a5af2ca963 chore: bundles do change a tiny bit now 2025-07-24 19:14:33 +10:00
Marais Rossouw 7e2aa19f3b chore: use github's shared prettier-config 2025-07-24 19:11:15 +10:00
Marais Rossouw a2235c5511 chore: move superlinter files to .github/linters 2025-07-24 19:11:03 +10:00
Marais Rossouw b1fc21bd19 Merge pull request #70 from actions/mr/to-vitest 2025-07-24 19:06:21 +10:00
Marais Rossouw 305e9d3933 chore: trigger ci 2025-07-24 19:05:42 +10:00
licensed-ci b1c0a96f18 Auto-update license files 2025-07-24 08:21:55 +00:00
Marais Rossouw ea289a3b79 chore: move dev deps to dev deps 2025-07-24 18:20:00 +10:00
Marais Rossouw 77f8afc857 chore: drop coverage, for now 2025-07-24 18:17:25 +10:00
Marais Rossouw 4ba8e6bc1e feat: moves project to using vitest 2025-07-24 18:08:26 +10:00
Marais Rossouw 64cbe74d35 Merge pull request #69 from actions/mr/cleanup-package.json
Tidy up package.json
2025-07-24 08:19:08 +10:00
Marais Rossouw d045ae4018 chore: tidy up package.json 2025-07-24 08:04:24 +10:00
Yuzuki 0b15edbb56 Merge pull request #66 from actions/dependabot/npm_and_yarn/multi-da3791aed2
Bump jest and @types/jest
2025-07-23 15:38:35 +10:00
Yuzuki 8726487e22 Merge branch 'main' into dependabot/npm_and_yarn/multi-da3791aed2 2025-07-23 15:34:29 +10:00
Yuzuki 79c7fc388f Merge pull request #65 from actions/dependabot/npm_and_yarn/rollup/rollup-linux-x64-gnu-4.45.1
Bump @rollup/rollup-linux-x64-gnu from 4.43.0 to 4.45.1
2025-07-22 11:34:41 +10:00
dependabot[bot] e43f4c40d0 Bump jest and @types/jest
---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.0.4
  dependency-type: direct:development
  update-type: version-update:semver-major
- dependency-name: "@types/jest"
  dependency-version: 30.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 05:53:19 +00:00
dependabot[bot] 7396fddf1d Bump @rollup/rollup-linux-x64-gnu from 4.43.0 to 4.45.1
---
updated-dependencies:
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.45.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 05:53:04 +00:00
Yuzuki afe6f4df95 Merge pull request #63 from actions/dependabot/npm_and_yarn/github/local-action-5.1.0
Bump @github/local-action from 3.2.1 to 5.1.0
2025-07-21 15:51:49 +10:00
dependabot[bot] a915345307 Bump @github/local-action from 3.2.1 to 5.1.0
---
updated-dependencies:
- dependency-name: "@github/local-action"
  dependency-version: 5.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 05:16:08 +00:00
40 changed files with 1078 additions and 6272 deletions
+2 -3
View File
@@ -43,7 +43,7 @@ jobs:
- name: Test
id: npm-ci-test
run: npm run ci-test
run: npm run test
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -83,8 +83,7 @@ jobs:
run: echo "hello" > prompt.txt
- name: Create System Prompt File
run:
echo "You are a helpful AI assistant for testing." > system-prompt.txt
run: echo "You are a helpful AI assistant for testing." > system-prompt.txt
- name: Test Local Action with Prompt File
id: test-action-prompt-file
@@ -11,12 +11,11 @@ permissions:
jobs:
update_tag:
name:
Update the major tag to include the ${{ github.event.release.tag_name }}
name: Update the major tag to include the ${{ github.event.release.tag_name }}
changes
runs-on: ubuntu-latest
steps:
- name: Update the ${{ env.TAG_NAME }} tag
uses: actions/publish-action@v0.2.2
uses: actions/publish-action@v0.3.0
with:
source-tag: ${{ env.TAG_NAME }}
-32
View File
@@ -1,32 +0,0 @@
---
name: "@rollup/plugin-json"
version: 6.1.0
type: npm
summary: Convert .json files to ES6 modules
homepage: https://github.com/rollup/plugins/tree/master/packages/json#readme
license: mit
licenses:
- sources: LICENSE
text: |
The MIT License (MIT)
Copyright (c) 2019 RollupJS Plugin Contributors (https://github.com/rollup/plugins/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
notices: []
-32
View File
@@ -1,32 +0,0 @@
---
name: "@rollup/pluginutils"
version: 5.1.4
type: npm
summary: A set of utility functions commonly used by Rollup plugins
homepage: https://github.com/rollup/plugins/tree/master/packages/pluginutils#readme
license: mit
licenses:
- sources: LICENSE
text: |
The MIT License (MIT)
Copyright (c) 2019 RollupJS Plugin Contributors (https://github.com/rollup/plugins/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
notices: []
@@ -1,30 +0,0 @@
---
name: "@rollup/rollup-linux-x64-musl"
version: 4.43.0
type: npm
summary: Native bindings for Rollup
homepage: https://rollupjs.org/
license: mit
licenses:
- sources: Auto-generated MIT license text
text: |
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
notices: []
-32
View File
@@ -1,32 +0,0 @@
---
name: "@types/estree"
version: 1.0.7
type: npm
summary: TypeScript definitions for estree
homepage: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree
license: mit
licenses:
- sources: LICENSE
text: |2
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
notices: []
-32
View File
@@ -1,32 +0,0 @@
---
name: "@types/js-yaml"
version: 4.0.9
type: npm
summary: TypeScript definitions for js-yaml
homepage: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/js-yaml
license: mit
licenses:
- sources: LICENSE
text: |2
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
notices: []
-20
View File
@@ -1,20 +0,0 @@
---
name: estree-walker
version: 2.0.2
type: npm
summary: Traverse an ESTree-compliant AST
homepage:
license: mit
licenses:
- sources: LICENSE
text: |-
Copyright (c) 2015-20 [these people](https://github.com/Rich-Harris/estree-walker/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- sources: README.md
text: MIT
notices: []
-38
View File
@@ -1,38 +0,0 @@
---
name: picomatch
version: 4.0.2
type: npm
summary: Blazing fast and accurate glob matcher written in JavaScript, with no dependencies
and full support for standard and extended Bash glob features, including braces,
extglobs, POSIX brackets, and regular expressions.
homepage: https://github.com/micromatch/picomatch
license: mit
licenses:
- sources: LICENSE
text: |
The MIT License (MIT)
Copyright (c) 2017-present, Jon Schlinkert.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
- sources: README.md
text: |-
Copyright © 2017-present, [Jon Schlinkert](https://github.com/jonschlinkert).
Released under the [MIT License](LICENSE).
notices: []
-438
View File
@@ -1,438 +0,0 @@
---
name: rollup
version: 4.43.0
type: npm
summary: Next-generation ES module bundler
homepage: https://rollupjs.org/
license: other
licenses:
- sources: LICENSE.md
text: "# Rollup core license\nRollup is released under the MIT license:\n\nThe MIT
License (MIT)\n\nCopyright (c) 2017 [these people](https://github.com/rollup/rollup/graphs/contributors)\n\nPermission
is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the \"Software\"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following
conditions:\n\nThe above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE
IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n# Licenses of bundled dependencies\nThe
published Rollup artifact additionally contains code with the following licenses:\nMIT,
ISC, 0BSD\n\n# Bundled dependencies:\n## @jridgewell/sourcemap-codec\nLicense:
MIT\nBy: Rich Harris\nRepository: git+https://github.com/jridgewell/sourcemap-codec.git\n\n>
The MIT License\n> \n> Copyright (c) 2015 Rich Harris\n> \n> Permission is hereby
granted, free of charge, to any person obtaining a copy\n> of this software and
associated documentation files (the \"Software\"), to deal\n> in the Software
without restriction, including without limitation the rights\n> to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell\n> copies of the Software,
and to permit persons to whom the Software is\n> furnished to do so, subject to
the following conditions:\n> \n> The above copyright notice and this permission
notice shall be included in\n> all copies or substantial portions of the Software.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n>
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n>
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
@rollup/pluginutils\nLicense: MIT\nBy: Rich Harris\nRepository: rollup/plugins\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2019 RollupJS Plugin Contributors (https://github.com/rollup/plugins/graphs/contributors)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy\n>
of this software and associated documentation files (the \"Software\"), to deal\n>
in the Software without restriction, including without limitation the rights\n>
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n> copies
of the Software, and to permit persons to whom the Software is\n> furnished to
do so, subject to the following conditions:\n> \n> The above copyright notice
and this permission notice shall be included in\n> all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## anymatch\nLicense:
ISC\nBy: Elan Shanker\nRepository: https://github.com/micromatch/anymatch\n\n>
The ISC License\n> \n> Copyright (c) 2019 Elan Shanker, Paul Miller (https://paulmillr.com)\n>
\n> Permission to use, copy, modify, and/or distribute this software for any\n>
purpose with or without fee is hereby granted, provided that the above\n> copyright
notice and this permission notice appear in all copies.\n> \n> THE SOFTWARE IS
PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n> WITH REGARD TO THIS
SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n> MERCHANTABILITY AND FITNESS. IN
NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES\n> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN\n> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
OUT OF OR\n> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n---------------------------------------\n\n##
binary-extensions\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/binary-extensions\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
Copyright (c) Paul Miller (https://paulmillr.com)\n> \n> Permission is hereby
granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the \"Software\"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:\n>
\n> The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED
\"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
braces\nLicense: MIT\nBy: Jon Schlinkert, Brian Woodward, Elan Shanker, Eugene
Sharygin, hemanth.hm\nRepository: micromatch/braces\n\n> The MIT License (MIT)\n>
\n> Copyright (c) 2014-present, Jon Schlinkert.\n> \n> Permission is hereby granted,
free of charge, to any person obtaining a copy\n> of this software and associated
documentation files (the \"Software\"), to deal\n> in the Software without restriction,
including without limitation the rights\n> to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell\n> copies of the Software, and to permit persons
to whom the Software is\n> furnished to do so, subject to the following conditions:\n>
\n> The above copyright notice and this permission notice shall be included in\n>
all copies or substantial portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED
\"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
builtin-modules\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/builtin-modules\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
chokidar\nLicense: MIT\nBy: Paul Miller, Elan Shanker\nRepository: git+https://github.com/paulmillr/chokidar.git\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2012-2019 Paul Miller (https://paulmillr.com),
Elan Shanker\n> \n> Permission is hereby granted, free of charge, to any person
obtaining a copy\n> of this software and associated documentation files (the “Software”),
to deal\n> in the Software without restriction, including without limitation the
rights\n> to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell\n> copies of the Software, and to permit persons to whom the Software is\n>
furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in\n> all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## date-time\nLicense:
MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/date-time\n\n> MIT License\n>
\n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
fill-range\nLicense: MIT\nBy: Jon Schlinkert, Edo Rivai, Paul Miller, Rouven Weßling\nRepository:
jonschlinkert/fill-range\n\n> The MIT License (MIT)\n> \n> Copyright (c) 2014-present,
Jon Schlinkert.\n> \n> Permission is hereby granted, free of charge, to any person
obtaining a copy\n> of this software and associated documentation files (the \"Software\"),
to deal\n> in the Software without restriction, including without limitation the
rights\n> to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell\n> copies of the Software, and to permit persons to whom the Software is\n>
furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in\n> all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## flru\nLicense:
MIT\nBy: Luke Edwards\nRepository: lukeed/flru\n\n> MIT License\n> \n> Copyright
(c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)\n> \n> Permission is
hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the \"Software\"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following
conditions:\n> \n> The above copyright notice and this permission notice shall
be included in all copies or substantial portions of the Software.\n> \n> THE
SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
glob-parent\nLicense: ISC\nBy: Gulp Team, Elan Shanker, Blaine Bublitz\nRepository:
gulpjs/glob-parent\n\n> The ISC License\n> \n> Copyright (c) 2015, 2019 Elan Shanker\n>
\n> Permission to use, copy, modify, and/or distribute this software for any\n>
purpose with or without fee is hereby granted, provided that the above\n> copyright
notice and this permission notice appear in all copies.\n> \n> THE SOFTWARE IS
PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n> WITH REGARD TO THIS
SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n> MERCHANTABILITY AND FITNESS. IN
NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES\n> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN\n> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
OUT OF OR\n> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n---------------------------------------\n\n##
is-binary-path\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/is-binary-path\n\n>
MIT License\n> \n> Copyright (c) 2019 Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com),
Paul Miller (https://paulmillr.com)\n> \n> Permission is hereby granted, free
of charge, to any person obtaining a copy of this software and associated documentation
files (the \"Software\"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom
the Software is furnished to do so, subject to the following conditions:\n> \n>
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED
\"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
is-extglob\nLicense: MIT\nBy: Jon Schlinkert\nRepository: jonschlinkert/is-extglob\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2014-2016, Jon Schlinkert\n> \n> Permission
is hereby granted, free of charge, to any person obtaining a copy\n> of this software
and associated documentation files (the \"Software\"), to deal\n> in the Software
without restriction, including without limitation the rights\n> to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell\n> copies of the Software,
and to permit persons to whom the Software is\n> furnished to do so, subject to
the following conditions:\n> \n> The above copyright notice and this permission
notice shall be included in\n> all copies or substantial portions of the Software.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n>
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n>
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
is-glob\nLicense: MIT\nBy: Jon Schlinkert, Brian Woodward, Daniel Perez\nRepository:
micromatch/is-glob\n\n> The MIT License (MIT)\n> \n> Copyright (c) 2014-2017,
Jon Schlinkert.\n> \n> Permission is hereby granted, free of charge, to any person
obtaining a copy\n> of this software and associated documentation files (the \"Software\"),
to deal\n> in the Software without restriction, including without limitation the
rights\n> to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell\n> copies of the Software, and to permit persons to whom the Software is\n>
furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in\n> all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## is-number\nLicense:
MIT\nBy: Jon Schlinkert, Olsten Larck, Rouven Weßling\nRepository: jonschlinkert/is-number\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2014-present, Jon Schlinkert.\n> \n>
Permission is hereby granted, free of charge, to any person obtaining a copy\n>
of this software and associated documentation files (the \"Software\"), to deal\n>
in the Software without restriction, including without limitation the rights\n>
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n> copies
of the Software, and to permit persons to whom the Software is\n> furnished to
do so, subject to the following conditions:\n> \n> The above copyright notice
and this permission notice shall be included in\n> all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## is-reference\nLicense:
MIT\nBy: Rich Harris\nRepository: git+https://github.com/Rich-Harris/is-reference.git\n\n---------------------------------------\n\n##
locate-character\nLicense: MIT\nBy: Rich Harris\nRepository: git+https://gitlab.com/Rich-Harris/locate-character.git\n\n---------------------------------------\n\n##
magic-string\nLicense: MIT\nBy: Rich Harris\nRepository: https://github.com/rich-harris/magic-string\n\n>
Copyright 2018 Rich Harris\n> \n> Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and associated documentation files
(the \"Software\"), to deal in the Software without restriction, including without
limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.\n\n---------------------------------------\n\n## normalize-path\nLicense:
MIT\nBy: Jon Schlinkert, Blaine Bublitz\nRepository: jonschlinkert/normalize-path\n\n>
The MIT License (MIT)\n> \n> Copyright (c) 2014-2018, Jon Schlinkert.\n> \n> Permission
is hereby granted, free of charge, to any person obtaining a copy\n> of this software
and associated documentation files (the \"Software\"), to deal\n> in the Software
without restriction, including without limitation the rights\n> to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell\n> copies of the Software,
and to permit persons to whom the Software is\n> furnished to do so, subject to
the following conditions:\n> \n> The above copyright notice and this permission
notice shall be included in\n> all copies or substantial portions of the Software.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n>
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n>
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
parse-ms\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/parse-ms\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
picocolors\nLicense: ISC\nBy: Alexey Raspopov\nRepository: alexeyraspopov/picocolors\n\n>
ISC License\n> \n> Copyright (c) 2021-2024 Oleksii Raspopov, Kostiantyn Denysov,
Anton Verinov\n> \n> Permission to use, copy, modify, and/or distribute this software
for any\n> purpose with or without fee is hereby granted, provided that the above\n>
copyright notice and this permission notice appear in all copies.\n> \n> THE SOFTWARE
IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n> WITH REGARD TO
THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n> MERCHANTABILITY AND FITNESS.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n> ANY SPECIAL, DIRECT, INDIRECT, OR
CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n> WHATSOEVER RESULTING FROM LOSS OF USE,
DATA OR PROFITS, WHETHER IN AN\n> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
ACTION, ARISING OUT OF\n> OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
SOFTWARE.\n\n---------------------------------------\n\n## picomatch\nLicense:
MIT\nBy: Jon Schlinkert\nRepository: micromatch/picomatch\n\n> The MIT License
(MIT)\n> \n> Copyright (c) 2017-present, Jon Schlinkert.\n> \n> Permission is
hereby granted, free of charge, to any person obtaining a copy\n> of this software
and associated documentation files (the \"Software\"), to deal\n> in the Software
without restriction, including without limitation the rights\n> to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell\n> copies of the Software,
and to permit persons to whom the Software is\n> furnished to do so, subject to
the following conditions:\n> \n> The above copyright notice and this permission
notice shall be included in\n> all copies or substantial portions of the Software.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n>
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n>
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n> LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n> OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n##
pretty-bytes\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/pretty-bytes\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
pretty-ms\nLicense: MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/pretty-ms\n\n>
MIT License\n> \n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
readdirp\nLicense: MIT\nBy: Thorsten Lorenz, Paul Miller\nRepository: git://github.com/paulmillr/readdirp.git\n\n>
MIT License\n> \n> Copyright (c) 2012-2019 Thorsten Lorenz, Paul Miller (https://paulmillr.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy\n>
of this software and associated documentation files (the \"Software\"), to deal\n>
in the Software without restriction, including without limitation the rights\n>
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n> copies
of the Software, and to permit persons to whom the Software is\n> furnished to
do so, subject to the following conditions:\n> \n> The above copyright notice
and this permission notice shall be included in all\n> copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE\n> SOFTWARE.\n\n---------------------------------------\n\n## signal-exit\nLicense:
ISC\nBy: Ben Coe\nRepository: https://github.com/tapjs/signal-exit.git\n\n> The
ISC License\n> \n> Copyright (c) 2015-2023 Benjamin Coe, Isaac Z. Schlueter, and
Contributors\n> \n> Permission to use, copy, modify, and/or distribute this software\n>
for any purpose with or without fee is hereby granted, provided\n> that the above
copyright notice and this permission notice\n> appear in all copies.\n> \n> THE
SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n> WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES\n> OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE\n> LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES\n> OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS,\n> WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION,\n> ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
OF THIS SOFTWARE.\n\n---------------------------------------\n\n## time-zone\nLicense:
MIT\nBy: Sindre Sorhus\nRepository: sindresorhus/time-zone\n\n> MIT License\n>
\n> Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n>
\n> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:\n> \n> The above copyright notice and
this permission notice shall be included in all copies or substantial portions
of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---------------------------------------\n\n##
to-regex-range\nLicense: MIT\nBy: Jon Schlinkert, Rouven Weßling\nRepository:
micromatch/to-regex-range\n\n> The MIT License (MIT)\n> \n> Copyright (c) 2015-present,
Jon Schlinkert.\n> \n> Permission is hereby granted, free of charge, to any person
obtaining a copy\n> of this software and associated documentation files (the \"Software\"),
to deal\n> in the Software without restriction, including without limitation the
rights\n> to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell\n> copies of the Software, and to permit persons to whom the Software is\n>
furnished to do so, subject to the following conditions:\n> \n> The above copyright
notice and this permission notice shall be included in\n> all copies or substantial
portions of the Software.\n> \n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR\n> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY,\n> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE\n> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER\n> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,\n> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN\n> THE SOFTWARE.\n\n---------------------------------------\n\n## tslib\nLicense:
0BSD\nBy: Microsoft Corp.\nRepository: https://github.com/Microsoft/tslib.git\n\n>
Copyright (c) Microsoft Corporation.\n> \n> Permission to use, copy, modify, and/or
distribute this software for any\n> purpose with or without fee is hereby granted.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH\n> REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\n>
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n>
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\n>
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\n>
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\n> PERFORMANCE
OF THIS SOFTWARE.\n\n---------------------------------------\n\n## yargs-parser\nLicense:
ISC\nBy: Ben Coe\nRepository: https://github.com/yargs/yargs-parser.git\n\n> Copyright
(c) 2016, Contributors\n> \n> Permission to use, copy, modify, and/or distribute
this software\n> for any purpose with or without fee is hereby granted, provided\n>
that the above copyright notice and this permission notice\n> appear in all copies.\n>
\n> THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n>
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES\n> OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE\n> LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES\n> OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS,\n> WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION,\n> ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
OF THIS SOFTWARE.\n"
- sources: README.md
text: "[MIT](https://github.com/rollup/rollup/blob/master/LICENSE.md)"
notices: []
-16
View File
@@ -1,16 +0,0 @@
# See: https://prettier.io/docs/en/configuration
printWidth: 80
tabWidth: 2
useTabs: false
semi: false
singleQuote: true
quoteProps: as-needed
jsxSingleQuote: false
trailingComma: none
bracketSpacing: true
bracketSameLine: true
arrowParens: always
proseWrap: always
htmlWhitespaceSensitivity: css
endOfLine: lf
+17 -6
View File
@@ -4,7 +4,6 @@
![CI](https://github.com/actions/typescript-action/actions/workflows/ci.yml/badge.svg)
[![Check dist/](https://github.com/actions/typescript-action/actions/workflows/check-dist.yml/badge.svg)](https://github.com/actions/typescript-action/actions/workflows/check-dist.yml)
[![CodeQL](https://github.com/actions/typescript-action/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/actions/typescript-action/actions/workflows/codeql-analysis.yml)
[![Coverage](./badges/coverage.svg)](./badges/coverage.svg)
Use AI models from [GitHub Models](https://github.com/marketplace/models) in
your workflows.
@@ -84,8 +83,7 @@ model: openai/gpt-4o
```yaml
messages:
- role: system
content:
You are a helpful assistant that describes animals using JSON format
content: You are a helpful assistant that describes animals using JSON format
- role: user
content: |-
Describe a {{animal}}
@@ -170,12 +168,24 @@ steps:
token: ${{ secrets.USER_PAT }}
```
If you want, you can use separate tokens for the AI inference endpoint
and the GitHub MCP server:
```yaml
steps:
- name: AI Inference with Separate MCP Token
id: inference
uses: actions/ai-inference@v1.2
with:
prompt: 'List my open pull requests and create a summary'
enable-github-mcp: true
token: ${{ secrets.GITHUB_TOKEN }}
github-mcp-token: ${{ secrets.USER_PAT }}
```
When MCP is enabled, the AI model will have access to GitHub tools and can
perform actions like searching issues and PRs.
**Note:** For now, MCP integration cannot be used with the built-in token. You
must pass a GitHub PAT into `token:` instead.
## Inputs
Various inputs are defined in [`action.yml`](action.yml) to let you configure
@@ -193,6 +203,7 @@ the action:
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
| `max-tokens` | The max number of tokens to generate | 200 |
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). Use a separate PAT for tighter security | `""` |
## Outputs
+9 -9
View File
@@ -1,11 +1,11 @@
import type * as core from '@actions/core'
import { jest } from '@jest/globals'
import {vi} from 'vitest'
export const debug = jest.fn<typeof core.debug>()
export const error = jest.fn<typeof core.error>()
export const info = jest.fn<typeof core.info>()
export const getInput = jest.fn<typeof core.getInput>()
export const getBooleanInput = jest.fn<typeof core.getBooleanInput>()
export const setOutput = jest.fn<typeof core.setOutput>()
export const setFailed = jest.fn<typeof core.setFailed>()
export const warning = jest.fn<typeof core.warning>()
export const debug = vi.fn<typeof core.debug>()
export const error = vi.fn<typeof core.error>()
export const info = vi.fn<typeof core.info>()
export const getInput = vi.fn<typeof core.getInput>()
export const getBooleanInput = vi.fn<typeof core.getBooleanInput>()
export const setOutput = vi.fn<typeof core.setOutput>()
export const setFailed = vi.fn<typeof core.setFailed>()
export const warning = vi.fn<typeof core.warning>()
+1 -2
View File
@@ -1,7 +1,6 @@
messages:
- role: system
content:
You are a helpful assistant that describes animals using JSON format
content: You are a helpful assistant that describes animals using JSON format
- role: user
content: |-
Describe a {{animal}}
+2 -2
View File
@@ -1,3 +1,3 @@
import { jest } from '@jest/globals'
import {vi} from 'vitest'
export const wait = jest.fn<typeof import('../src/wait.js').wait>()
export const wait = vi.fn<typeof import('../src/wait.js').wait>()
+34 -40
View File
@@ -1,41 +1,37 @@
import { describe, it, expect } from '@jest/globals'
import {
buildMessages,
buildResponseFormat,
buildInferenceRequest
} from '../src/helpers'
import { PromptConfig } from '../src/prompt'
import {describe, it, expect} from 'vitest'
import {buildMessages, buildResponseFormat, buildInferenceRequest} from '../src/helpers'
import {PromptConfig} from '../src/prompt'
describe('helpers.ts - inference request building', () => {
describe('buildMessages', () => {
it('should build messages from prompt config', () => {
const promptConfig: PromptConfig = {
messages: [
{ role: 'system', content: 'System message' },
{ role: 'user', content: 'User message' }
]
{role: 'system', content: 'System message'},
{role: 'user', content: 'User message'},
],
}
const result = buildMessages(promptConfig)
expect(result).toEqual([
{ role: 'system', content: 'System message' },
{ role: 'user', content: 'User message' }
{role: 'system', content: 'System message'},
{role: 'user', content: 'User message'},
])
})
it('should build messages from legacy format', () => {
const result = buildMessages(undefined, 'System prompt', 'User prompt')
expect(result).toEqual([
{ role: 'system', content: 'System prompt' },
{ role: 'user', content: 'User prompt' }
{role: 'system', content: 'System prompt'},
{role: 'user', content: 'User prompt'},
])
})
it('should use default system prompt when none provided', () => {
const result = buildMessages(undefined, undefined, 'User prompt')
expect(result).toEqual([
{ role: 'system', content: 'You are a helpful assistant' },
{ role: 'user', content: 'User prompt' }
{role: 'system', content: 'You are a helpful assistant'},
{role: 'user', content: 'User prompt'},
])
})
})
@@ -47,8 +43,8 @@ describe('helpers.ts - inference request building', () => {
responseFormat: 'json_schema',
jsonSchema: JSON.stringify({
name: 'test_schema',
schema: { type: 'object' }
})
schema: {type: 'object'},
}),
}
const result = buildResponseFormat(promptConfig)
@@ -56,15 +52,15 @@ describe('helpers.ts - inference request building', () => {
type: 'json_schema',
json_schema: {
name: 'test_schema',
schema: { type: 'object' }
}
schema: {type: 'object'},
},
})
})
it('should return undefined for text format', () => {
const promptConfig: PromptConfig = {
messages: [],
responseFormat: 'text'
responseFormat: 'text',
}
const result = buildResponseFormat(promptConfig)
@@ -73,7 +69,7 @@ describe('helpers.ts - inference request building', () => {
it('should return undefined when no response format specified', () => {
const promptConfig: PromptConfig = {
messages: []
messages: [],
}
const result = buildResponseFormat(promptConfig)
@@ -84,12 +80,10 @@ describe('helpers.ts - inference request building', () => {
const promptConfig: PromptConfig = {
messages: [],
responseFormat: 'json_schema',
jsonSchema: 'invalid json'
jsonSchema: 'invalid json',
}
expect(() => buildResponseFormat(promptConfig)).toThrow(
'Invalid JSON schema'
)
expect(() => buildResponseFormat(promptConfig)).toThrow('Invalid JSON schema')
})
})
@@ -97,14 +91,14 @@ describe('helpers.ts - inference request building', () => {
it('should build complete inference request from prompt config', () => {
const promptConfig: PromptConfig = {
messages: [
{ role: 'system', content: 'System message' },
{ role: 'user', content: 'User message' }
{role: 'system', content: 'System message'},
{role: 'user', content: 'User message'},
],
responseFormat: 'json_schema',
jsonSchema: JSON.stringify({
name: 'test_schema',
schema: { type: 'object' }
})
schema: {type: 'object'},
}),
}
const result = buildInferenceRequest(
@@ -114,13 +108,13 @@ describe('helpers.ts - inference request building', () => {
'gpt-4',
100,
'https://api.test.com',
'test-token'
'test-token',
)
expect(result).toEqual({
messages: [
{ role: 'system', content: 'System message' },
{ role: 'user', content: 'User message' }
{role: 'system', content: 'System message'},
{role: 'user', content: 'User message'},
],
modelName: 'gpt-4',
maxTokens: 100,
@@ -130,9 +124,9 @@ describe('helpers.ts - inference request building', () => {
type: 'json_schema',
json_schema: {
name: 'test_schema',
schema: { type: 'object' }
}
}
schema: {type: 'object'},
},
},
})
})
@@ -144,19 +138,19 @@ describe('helpers.ts - inference request building', () => {
'gpt-4',
100,
'https://api.test.com',
'test-token'
'test-token',
)
expect(result).toEqual({
messages: [
{ role: 'system', content: 'System prompt' },
{ role: 'user', content: 'User prompt' }
{role: 'system', content: 'System prompt'},
{role: 'user', content: 'User prompt'},
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'test-token',
responseFormat: undefined
responseFormat: undefined,
})
})
})
+10 -23
View File
@@ -1,26 +1,21 @@
/**
* Unit tests for the helpers module, src/helpers.ts
*/
import { jest } from '@jest/globals'
import {vi, it, expect, beforeEach, describe} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Mock fs module
const mockExistsSync = jest.fn()
const mockReadFileSync = jest.fn()
const mockExistsSync = vi.fn()
const mockReadFileSync = vi.fn()
jest.unstable_mockModule('fs', () => ({
vi.mock('fs', () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync
readFileSync: mockReadFileSync,
}))
jest.unstable_mockModule('@actions/core', () => core)
vi.mock('@actions/core', () => core)
// Import the module being tested
const { loadContentFromFileOrInput } = await import('../src/helpers.js')
const {loadContentFromFileOrInput} = await import('../src/helpers.js')
describe('helpers.ts', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('loadContentFromFileOrInput', () => {
@@ -108,11 +103,7 @@ describe('helpers.ts', () => {
core.getInput.mockImplementation(() => '')
const result = loadContentFromFileOrInput(
'file-input',
'content-input',
defaultValue
)
const result = loadContentFromFileOrInput('file-input', 'content-input', defaultValue)
expect(result).toBe(defaultValue)
expect(mockExistsSync).not.toHaveBeenCalled()
@@ -136,11 +127,7 @@ describe('helpers.ts', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
core.getInput.mockImplementation(() => undefined as any)
const result = loadContentFromFileOrInput(
'file-input',
'content-input',
defaultValue
)
const result = loadContentFromFileOrInput('file-input', 'content-input', defaultValue)
expect(result).toBe(defaultValue)
})
+94 -118
View File
@@ -1,50 +1,45 @@
/**
* Unit tests for the inference module, src/inference.ts
*/
import { jest } from '@jest/globals'
import {vi, type MockedFunction, beforeEach, expect, describe, it} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Mock Azure AI Inference
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockPost = jest.fn() as jest.MockedFunction<any>
const mockPath = jest.fn(() => ({ post: mockPost }))
const mockClient = jest.fn(() => ({ path: mockPath }))
const mockPost = vi.fn() as MockedFunction<any>
const mockPath = vi.fn(() => ({post: mockPost}))
const mockClient = vi.fn(() => ({path: mockPath}))
jest.unstable_mockModule('@azure-rest/ai-inference', () => ({
vi.mock('@azure-rest/ai-inference', () => ({
default: mockClient,
isUnexpected: jest.fn(() => false)
isUnexpected: vi.fn(() => false),
}))
jest.unstable_mockModule('@azure/core-auth', () => ({
AzureKeyCredential: jest.fn()
vi.mock('@azure/core-auth', () => ({
AzureKeyCredential: vi.fn(),
}))
// Mock MCP functions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockExecuteToolCalls = jest.fn() as jest.MockedFunction<any>
jest.unstable_mockModule('../src/mcp.js', () => ({
executeToolCalls: mockExecuteToolCalls
const mockExecuteToolCalls = vi.fn() as MockedFunction<any>
vi.mock('../src/mcp.js', () => ({
executeToolCalls: mockExecuteToolCalls,
}))
jest.unstable_mockModule('@actions/core', () => core)
vi.mock('@actions/core', () => core)
// Import the module being tested
const { simpleInference, mcpInference } = await import('../src/inference.js')
const {simpleInference, mcpInference} = await import('../src/inference.js')
describe('inference.ts', () => {
const mockRequest = {
messages: [
{ role: 'system', content: 'You are a test assistant' },
{ role: 'user', content: 'Hello, AI!' }
{role: 'system', content: 'You are a test assistant'},
{role: 'user', content: 'Hello, AI!'},
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'test-token'
token: 'test-token',
}
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('simpleInference', () => {
@@ -54,11 +49,11 @@ describe('inference.ts', () => {
choices: [
{
message: {
content: 'Hello, user!'
}
}
]
}
content: 'Hello, user!',
},
},
],
},
}
mockPost.mockResolvedValue(mockResponse)
@@ -66,9 +61,7 @@ describe('inference.ts', () => {
const result = await simpleInference(mockRequest)
expect(result).toBe('Hello, user!')
expect(core.info).toHaveBeenCalledWith(
'Running simple inference without tools'
)
expect(core.info).toHaveBeenCalledWith('Running simple inference without tools')
expect(core.info).toHaveBeenCalledWith('Model response: Hello, user!')
// Verify the request structure
@@ -77,16 +70,16 @@ describe('inference.ts', () => {
messages: [
{
role: 'system',
content: 'You are a test assistant'
content: 'You are a test assistant',
},
{
role: 'user',
content: 'Hello, AI!'
}
content: 'Hello, AI!',
},
],
max_tokens: 100,
model: 'gpt-4'
}
model: 'gpt-4',
},
})
})
@@ -96,11 +89,11 @@ describe('inference.ts', () => {
choices: [
{
message: {
content: null
}
}
]
}
content: null,
},
},
],
},
}
mockPost.mockResolvedValue(mockResponse)
@@ -108,9 +101,7 @@ describe('inference.ts', () => {
const result = await simpleInference(mockRequest)
expect(result).toBeNull()
expect(core.info).toHaveBeenCalledWith(
'Model response: No response content'
)
expect(core.info).toHaveBeenCalledWith('Model response: No response content')
})
})
@@ -124,10 +115,10 @@ describe('inference.ts', () => {
function: {
name: 'test-tool',
description: 'A test tool',
parameters: { type: 'object' }
}
}
]
parameters: {type: 'object'},
},
},
],
}
it('performs inference without tool calls', async () => {
@@ -137,11 +128,11 @@ describe('inference.ts', () => {
{
message: {
content: 'Hello, user!',
tool_calls: null
}
}
]
}
tool_calls: null,
},
},
],
},
}
mockPost.mockResolvedValue(mockResponse)
@@ -149,13 +140,9 @@ describe('inference.ts', () => {
const result = await mcpInference(mockRequest, mockMcpClient)
expect(result).toBe('Hello, user!')
expect(core.info).toHaveBeenCalledWith(
'Running GitHub MCP inference with tools'
)
expect(core.info).toHaveBeenCalledWith('Running GitHub MCP inference with tools')
expect(core.info).toHaveBeenCalledWith('MCP inference iteration 1')
expect(core.info).toHaveBeenCalledWith(
'No tool calls requested, ending GitHub MCP inference loop'
)
expect(core.info).toHaveBeenCalledWith('No tool calls requested, ending GitHub MCP inference loop')
// The MCP inference loop will always add the assistant message, even when there are no tool calls
// So we don't check the exact messages, just that tools were included
@@ -173,9 +160,9 @@ describe('inference.ts', () => {
id: 'call-123',
function: {
name: 'test-tool',
arguments: '{"param": "value"}'
}
}
arguments: '{"param": "value"}',
},
},
]
const toolResults = [
@@ -183,8 +170,8 @@ describe('inference.ts', () => {
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: 'Tool result'
}
content: 'Tool result',
},
]
// First response with tool calls
@@ -194,11 +181,11 @@ describe('inference.ts', () => {
{
message: {
content: 'I need to use a tool.',
tool_calls: toolCalls
}
}
]
}
tool_calls: toolCalls,
},
},
],
},
}
// Second response after tool execution
@@ -208,26 +195,21 @@ describe('inference.ts', () => {
{
message: {
content: 'Here is the final answer.',
tool_calls: null
}
}
]
}
tool_calls: null,
},
},
],
},
}
mockPost
.mockResolvedValueOnce(firstResponse)
.mockResolvedValueOnce(secondResponse)
mockPost.mockResolvedValueOnce(firstResponse).mockResolvedValueOnce(secondResponse)
mockExecuteToolCalls.mockResolvedValue(toolResults)
const result = await mcpInference(mockRequest, mockMcpClient)
expect(result).toBe('Here is the final answer.')
expect(mockExecuteToolCalls).toHaveBeenCalledWith(
mockMcpClient.client,
toolCalls
)
expect(mockExecuteToolCalls).toHaveBeenCalledWith(mockMcpClient.client, toolCalls)
expect(mockPost).toHaveBeenCalledTimes(2)
// Verify the second call includes the conversation history
@@ -245,9 +227,9 @@ describe('inference.ts', () => {
id: 'call-123',
function: {
name: 'test-tool',
arguments: '{}'
}
}
arguments: '{}',
},
},
]
const toolResults = [
@@ -255,8 +237,8 @@ describe('inference.ts', () => {
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: 'Tool result'
}
content: 'Tool result',
},
]
// Always respond with tool calls to trigger infinite loop
@@ -266,11 +248,11 @@ describe('inference.ts', () => {
{
message: {
content: 'Using tool again.',
tool_calls: toolCalls
}
}
]
}
tool_calls: toolCalls,
},
},
],
},
}
mockPost.mockResolvedValue(responseWithToolCalls)
@@ -279,9 +261,7 @@ describe('inference.ts', () => {
const result = await mcpInference(mockRequest, mockMcpClient)
expect(mockPost).toHaveBeenCalledTimes(5) // Max iterations reached
expect(core.warning).toHaveBeenCalledWith(
'GitHub MCP inference loop exceeded maximum iterations (5)'
)
expect(core.warning).toHaveBeenCalledWith('GitHub MCP inference loop exceeded maximum iterations (5)')
expect(result).toBe('Using tool again.') // Last assistant message
})
@@ -292,11 +272,11 @@ describe('inference.ts', () => {
{
message: {
content: 'Hello, user!',
tool_calls: []
}
}
]
}
tool_calls: [],
},
},
],
},
}
mockPost.mockResolvedValue(mockResponse)
@@ -304,9 +284,7 @@ describe('inference.ts', () => {
const result = await mcpInference(mockRequest, mockMcpClient)
expect(result).toBe('Hello, user!')
expect(core.info).toHaveBeenCalledWith(
'No tool calls requested, ending GitHub MCP inference loop'
)
expect(core.info).toHaveBeenCalledWith('No tool calls requested, ending GitHub MCP inference loop')
expect(mockExecuteToolCalls).not.toHaveBeenCalled()
})
@@ -314,8 +292,8 @@ describe('inference.ts', () => {
const toolCalls = [
{
id: 'call-123',
function: { name: 'test-tool', arguments: '{}' }
}
function: {name: 'test-tool', arguments: '{}'},
},
]
const firstResponse = {
@@ -324,11 +302,11 @@ describe('inference.ts', () => {
{
message: {
content: 'First message',
tool_calls: toolCalls
}
}
]
}
tool_calls: toolCalls,
},
},
],
},
}
const secondResponse = {
@@ -337,24 +315,22 @@ describe('inference.ts', () => {
{
message: {
content: 'Second message',
tool_calls: toolCalls
}
}
]
}
tool_calls: toolCalls,
},
},
],
},
}
mockPost
.mockResolvedValueOnce(firstResponse)
.mockResolvedValue(secondResponse)
mockPost.mockResolvedValueOnce(firstResponse).mockResolvedValue(secondResponse)
mockExecuteToolCalls.mockResolvedValue([
{
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: 'result'
}
content: 'result',
},
])
const result = await mcpInference(mockRequest, mockMcpClient)
+29 -35
View File
@@ -1,46 +1,46 @@
import { describe, it, expect, beforeEach, jest } from '@jest/globals'
import {describe, it, expect, beforeEach, vi, type MockedFunction, type Mock} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Create fs mocks
const mockExistsSync = jest.fn()
const mockReadFileSync = jest.fn()
const mockWriteFileSync = jest.fn()
const mockExistsSync = vi.fn()
const mockReadFileSync = vi.fn()
const mockWriteFileSync = vi.fn()
// Create inference mocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockSimpleInference = jest.fn() as jest.MockedFunction<any>
const mockMcpInference = jest.fn()
const mockSimpleInference = vi.fn() as MockedFunction<any>
const mockMcpInference = vi.fn()
// Create MCP mocks
const mockConnectToGitHubMCP = jest.fn()
const mockConnectToGitHubMCP = vi.fn()
// Mock fs module
jest.unstable_mockModule('fs', () => ({
vi.mock('fs', () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync,
writeFileSync: mockWriteFileSync
writeFileSync: mockWriteFileSync,
}))
// Mock the inference functions
jest.unstable_mockModule('../src/inference.js', () => ({
vi.mock('../src/inference.js', () => ({
simpleInference: mockSimpleInference,
mcpInference: mockMcpInference
mcpInference: mockMcpInference,
}))
// Mock the MCP connection
jest.unstable_mockModule('../src/mcp.js', () => ({
connectToGitHubMCP: mockConnectToGitHubMCP
vi.mock('../src/mcp.js', () => ({
connectToGitHubMCP: mockConnectToGitHubMCP,
}))
jest.unstable_mockModule('@actions/core', () => core)
vi.mock('@actions/core', () => core)
// The module being tested should be imported dynamically. This ensures that the
// mocks are used in place of any actual dependencies.
const { run } = await import('../src/main.js')
const {run} = await import('../src/main.js')
describe('main.ts - prompt.yml integration', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
// Mock environment variables
process.env['GITHUB_TOKEN'] = 'test-token'
@@ -62,7 +62,7 @@ describe('main.ts - prompt.yml integration', () => {
})
// Mock core.getBooleanInput
const mockGetBooleanInput = core.getBooleanInput as jest.Mock
const mockGetBooleanInput = core.getBooleanInput as Mock
mockGetBooleanInput.mockReturnValue(false)
// Mock fs.readFileSync for prompt file
@@ -111,29 +111,23 @@ model: openai/gpt-4o
messages: [
{
role: 'system',
content: 'Be as concise as possible'
content: 'Be as concise as possible',
},
{
role: 'user',
content: 'Compare cats and dogs, please'
}
content: 'Compare cats and dogs, please',
},
],
modelName: 'openai/gpt-4o',
maxTokens: 200,
endpoint: 'https://models.github.ai/inference',
token: 'test-token'
})
token: 'test-token',
}),
)
// Verify outputs were set
expect(core.setOutput).toHaveBeenCalledWith(
'response',
'Mocked AI response'
)
expect(core.setOutput).toHaveBeenCalledWith(
'response-file',
expect.any(String)
)
expect(core.setOutput).toHaveBeenCalledWith('response', 'Mocked AI response')
expect(core.setOutput).toHaveBeenCalledWith('response-file', expect.any(String))
})
it('should fall back to legacy format when not using prompt YAML', async () => {
@@ -165,18 +159,18 @@ model: openai/gpt-4o
messages: [
{
role: 'system',
content: 'You are helpful'
content: 'You are helpful',
},
{
role: 'user',
content: 'Hello, world!'
}
content: 'Hello, world!',
},
],
modelName: 'openai/gpt-4o',
maxTokens: 200,
endpoint: 'https://models.github.ai/inference',
token: 'test-token'
})
token: 'test-token',
}),
)
})
})
+56 -67
View File
@@ -1,31 +1,21 @@
/**
* Unit tests for the action's main functionality, src/main.ts
*/
import { jest } from '@jest/globals'
import {vi, describe, expect, it, beforeEach, type MockedFunction} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Default to throwing errors to catch unexpected calls
const mockExistsSync = jest.fn().mockImplementation(() => {
throw new Error(
'Unexpected call to existsSync - test should override this implementation'
)
const mockExistsSync = vi.fn().mockImplementation(() => {
throw new Error('Unexpected call to existsSync - test should override this implementation')
})
const mockReadFileSync = jest.fn().mockImplementation(() => {
throw new Error(
'Unexpected call to readFileSync - test should override this implementation'
)
const mockReadFileSync = vi.fn().mockImplementation(() => {
throw new Error('Unexpected call to readFileSync - test should override this implementation')
})
const mockWriteFileSync = jest.fn()
const mockWriteFileSync = vi.fn()
/**
* Helper function to mock file system operations for one or more files
* @param fileContents - Object mapping file paths to their contents
* @param nonExistentFiles - Array of file paths that should be treated as non-existent
*/
function mockFileContent(
fileContents: Record<string, string> = {},
nonExistentFiles: string[] = []
): void {
function mockFileContent(fileContents: Record<string, string> = {}, nonExistentFiles: string[] = []): void {
// Mock existsSync to return true for files that exist, false for those that don't
mockExistsSync.mockImplementation((...args: unknown[]): boolean => {
const [path] = args as [string]
@@ -55,11 +45,11 @@ function mockInputs(inputs: Record<string, string> = {}): void {
token: 'fake-token',
model: 'gpt-4',
'max-tokens': '100',
endpoint: 'https://api.test.com'
endpoint: 'https://api.test.com',
}
// Combine defaults with user-provided inputs
const allInputs: Record<string, string> = { ...defaultInputs, ...inputs }
const allInputs: Record<string, string> = {...defaultInputs, ...inputs}
core.getInput.mockImplementation((name: string) => {
return allInputs[name] || ''
@@ -76,46 +66,48 @@ function mockInputs(inputs: Record<string, string> = {}): void {
*/
function verifyStandardResponse(): void {
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'response', 'Hello, user!')
expect(core.setOutput).toHaveBeenNthCalledWith(
2,
'response-file',
expect.stringContaining('modelResponse.txt')
)
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'response-file', expect.stringContaining('modelResponse.txt'))
}
jest.unstable_mockModule('fs', () => ({
vi.mock('fs', () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync,
writeFileSync: mockWriteFileSync
writeFileSync: mockWriteFileSync,
}))
// Mock MCP and inference modules
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockConnectToGitHubMCP = jest.fn() as jest.MockedFunction<any>
const mockConnectToGitHubMCP = vi.fn() as MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockSimpleInference = jest.fn() as jest.MockedFunction<any>
const mockSimpleInference = vi.fn() as MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockMcpInference = jest.fn() as jest.MockedFunction<any>
const mockMcpInference = vi.fn() as MockedFunction<any>
jest.unstable_mockModule('../src/mcp.js', () => ({
connectToGitHubMCP: mockConnectToGitHubMCP
vi.mock('../src/mcp.js', () => ({
connectToGitHubMCP: mockConnectToGitHubMCP,
}))
jest.unstable_mockModule('../src/inference.js', () => ({
vi.mock('../src/inference.js', () => ({
simpleInference: mockSimpleInference,
mcpInference: mockMcpInference
mcpInference: mockMcpInference,
}))
jest.unstable_mockModule('@actions/core', () => core)
vi.mock('@actions/core', () => core)
// Mock process.exit to prevent it from actually exiting during tests
const mockProcessExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called')
})
// The module being tested should be imported dynamically. This ensures that the
// mocks are used in place of any actual dependencies.
const { run } = await import('../src/main.js')
const {run} = await import('../src/main.js')
describe('main.ts', () => {
// Reset all mocks before each test
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockProcessExit.mockClear()
// Remove any existing GITHUB_TOKEN
delete process.env.GITHUB_TOKEN
@@ -128,7 +120,7 @@ describe('main.ts', () => {
it('Sets the response output', async () => {
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.'
'system-prompt': 'You are a test assistant.',
})
await run()
@@ -140,36 +132,35 @@ describe('main.ts', () => {
it('Sets a failed status when no prompt is set', async () => {
mockInputs({
prompt: '',
'prompt-file': ''
'prompt-file': '',
})
await run()
// Expect the run function to throw due to process.exit being mocked
await expect(run()).rejects.toThrow('process.exit called')
expect(core.setFailed).toHaveBeenNthCalledWith(
1,
'Neither prompt-file nor prompt was set'
)
expect(core.setFailed).toHaveBeenCalledWith('Neither prompt-file nor prompt was set')
expect(mockProcessExit).toHaveBeenCalledWith(1)
})
it('uses simple inference when MCP is disabled', async () => {
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.',
'enable-github-mcp': 'false'
'enable-github-mcp': 'false',
})
await run()
expect(mockSimpleInference).toHaveBeenCalledWith({
messages: [
{ role: 'system', content: 'You are a test assistant.' },
{ role: 'user', content: 'Hello, AI!' }
{role: 'system', content: 'You are a test assistant.'},
{role: 'user', content: 'Hello, AI!'},
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'fake-token',
responseFormat: undefined
responseFormat: undefined,
})
expect(mockConnectToGitHubMCP).not.toHaveBeenCalled()
expect(mockMcpInference).not.toHaveBeenCalled()
@@ -180,13 +171,13 @@ describe('main.ts', () => {
const mockMcpClient = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
client: {} as any,
tools: [{ type: 'function', function: { name: 'test-tool' } }]
tools: [{type: 'function', function: {name: 'test-tool'}}],
}
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.',
'enable-github-mcp': 'true'
'enable-github-mcp': 'true',
})
mockConnectToGitHubMCP.mockResolvedValue(mockMcpClient)
@@ -197,12 +188,12 @@ describe('main.ts', () => {
expect(mockMcpInference).toHaveBeenCalledWith(
expect.objectContaining({
messages: [
{ role: 'system', content: 'You are a test assistant.' },
{ role: 'user', content: 'Hello, AI!' }
{role: 'system', content: 'You are a test assistant.'},
{role: 'user', content: 'Hello, AI!'},
],
token: 'fake-token'
token: 'fake-token',
}),
mockMcpClient
mockMcpClient,
)
expect(mockSimpleInference).not.toHaveBeenCalled()
verifyStandardResponse()
@@ -212,7 +203,7 @@ describe('main.ts', () => {
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.',
'enable-github-mcp': 'true'
'enable-github-mcp': 'true',
})
mockConnectToGitHubMCP.mockResolvedValue(null)
@@ -222,9 +213,7 @@ describe('main.ts', () => {
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
expect(mockSimpleInference).toHaveBeenCalled()
expect(mockMcpInference).not.toHaveBeenCalled()
expect(core.warning).toHaveBeenCalledWith(
'MCP connection failed, falling back to simple inference'
)
expect(core.warning).toHaveBeenCalledWith('MCP connection failed, falling back to simple inference')
verifyStandardResponse()
})
@@ -236,27 +225,27 @@ describe('main.ts', () => {
mockFileContent({
[promptFile]: promptContent,
[systemPromptFile]: systemPromptContent
[systemPromptFile]: systemPromptContent,
})
mockInputs({
'prompt-file': promptFile,
'system-prompt-file': systemPromptFile,
'enable-github-mcp': 'false'
'enable-github-mcp': 'false',
})
await run()
expect(mockSimpleInference).toHaveBeenCalledWith({
messages: [
{ role: 'system', content: systemPromptContent },
{ role: 'user', content: promptContent }
{role: 'system', content: systemPromptContent},
{role: 'user', content: promptContent},
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'fake-token',
responseFormat: undefined
responseFormat: undefined,
})
verifyStandardResponse()
})
@@ -267,13 +256,13 @@ describe('main.ts', () => {
mockFileContent({}, [promptFile])
mockInputs({
'prompt-file': promptFile
'prompt-file': promptFile,
})
await run()
// Expect the run function to throw due to process.exit being mocked
await expect(run()).rejects.toThrow('process.exit called')
expect(core.setFailed).toHaveBeenCalledWith(
`File for prompt-file was not found: ${promptFile}`
)
expect(core.setFailed).toHaveBeenCalledWith(`File for prompt-file was not found: ${promptFile}`)
expect(mockProcessExit).toHaveBeenCalledWith(1)
})
})
+52 -82
View File
@@ -1,45 +1,37 @@
/**
* Unit tests for the MCP module, src/mcp.ts
*/
import { jest } from '@jest/globals'
import {vi, type MockedFunction, describe, it, expect, beforeEach} from 'vitest'
import * as core from '../__fixtures__/core.js'
// Mock MCP SDK
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockConnect = jest.fn() as jest.MockedFunction<any>
const mockConnect = vi.fn() as MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockListTools = jest.fn() as jest.MockedFunction<any>
const mockListTools = vi.fn() as MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockCallTool = jest.fn() as jest.MockedFunction<any>
const mockCallTool = vi.fn() as MockedFunction<any>
const mockClient = {
connect: mockConnect,
listTools: mockListTools,
callTool: mockCallTool
callTool: mockCallTool,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any
jest.unstable_mockModule('@modelcontextprotocol/sdk/client/index.js', () => ({
Client: jest.fn(() => mockClient)
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
Client: vi.fn(() => mockClient),
}))
jest.unstable_mockModule(
'@modelcontextprotocol/sdk/client/streamableHttp.js',
() => ({
StreamableHTTPClientTransport: jest.fn()
})
)
vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
StreamableHTTPClientTransport: vi.fn(),
}))
jest.unstable_mockModule('@actions/core', () => core)
vi.mock('@actions/core', () => core)
// Import the module being tested
const { connectToGitHubMCP, executeToolCall, executeToolCalls } = await import(
'../src/mcp.js'
)
const {connectToGitHubMCP, executeToolCall, executeToolCalls} = await import('../src/mcp.js')
describe('mcp.ts', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('connectToGitHubMCP', () => {
@@ -49,20 +41,20 @@ describe('mcp.ts', () => {
{
name: 'test-tool-1',
description: 'Test tool 1',
inputSchema: { type: 'object', properties: {} }
inputSchema: {type: 'object', properties: {}},
},
{
name: 'test-tool-2',
description: 'Test tool 2',
inputSchema: {
type: 'object',
properties: { param: { type: 'string' } }
}
}
properties: {param: {type: 'string'}},
},
},
]
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({ tools: mockTools })
mockListTools.mockResolvedValue({tools: mockTools})
const result = await connectToGitHubMCP(token)
@@ -74,21 +66,13 @@ describe('mcp.ts', () => {
function: {
name: 'test-tool-1',
description: 'Test tool 1',
parameters: { type: 'object', properties: {} }
}
parameters: {type: 'object', properties: {}},
},
})
expect(core.info).toHaveBeenCalledWith(
'Connecting to GitHub MCP server...'
)
expect(core.info).toHaveBeenCalledWith(
'Successfully connected to GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith(
'Retrieved 2 tools from GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith(
'Mapped 2 GitHub MCP tools for Azure AI Inference'
)
expect(core.info).toHaveBeenCalledWith('Connecting to GitHub MCP server...')
expect(core.info).toHaveBeenCalledWith('Successfully connected to GitHub MCP server')
expect(core.info).toHaveBeenCalledWith('Retrieved 2 tools from GitHub MCP server')
expect(core.info).toHaveBeenCalledWith('Mapped 2 GitHub MCP tools for Azure AI Inference')
})
it('returns null when connection fails', async () => {
@@ -100,27 +84,21 @@ describe('mcp.ts', () => {
const result = await connectToGitHubMCP(token)
expect(result).toBeNull()
expect(core.warning).toHaveBeenCalledWith(
'Failed to connect to GitHub MCP server: Error: Connection failed'
)
expect(core.warning).toHaveBeenCalledWith('Failed to connect to GitHub MCP server: Error: Connection failed')
})
it('handles empty tools list', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({ tools: [] })
mockListTools.mockResolvedValue({tools: []})
const result = await connectToGitHubMCP(token)
expect(result).not.toBeNull()
expect(result?.tools).toHaveLength(0)
expect(core.info).toHaveBeenCalledWith(
'Retrieved 0 tools from GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith(
'Mapped 0 GitHub MCP tools for Azure AI Inference'
)
expect(core.info).toHaveBeenCalledWith('Retrieved 0 tools from GitHub MCP server')
expect(core.info).toHaveBeenCalledWith('Mapped 0 GitHub MCP tools for Azure AI Inference')
})
it('handles undefined tools list', async () => {
@@ -133,9 +111,7 @@ describe('mcp.ts', () => {
expect(result).not.toBeNull()
expect(result?.tools).toHaveLength(0)
expect(core.info).toHaveBeenCalledWith(
'Retrieved 0 tools from GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith('Retrieved 0 tools from GitHub MCP server')
})
})
@@ -146,11 +122,11 @@ describe('mcp.ts', () => {
type: 'function',
function: {
name: 'test-tool',
arguments: '{"param": "value"}'
}
arguments: '{"param": "value"}',
},
}
const toolResult = {
content: [{ type: 'text', text: 'Tool execution result' }]
content: [{type: 'text', text: 'Tool execution result'}],
}
mockCallTool.mockResolvedValue(toolResult)
@@ -159,20 +135,16 @@ describe('mcp.ts', () => {
expect(mockCallTool).toHaveBeenCalledWith({
name: 'test-tool',
arguments: { param: 'value' }
arguments: {param: 'value'},
})
expect(result).toEqual({
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: JSON.stringify(toolResult.content)
content: JSON.stringify(toolResult.content),
})
expect(core.info).toHaveBeenCalledWith(
'Executing GitHub MCP tool: test-tool with args: {"param": "value"}'
)
expect(core.info).toHaveBeenCalledWith(
'GitHub MCP tool test-tool executed successfully'
)
expect(core.info).toHaveBeenCalledWith('Executing GitHub MCP tool: test-tool with args: {"param": "value"}')
expect(core.info).toHaveBeenCalledWith('GitHub MCP tool test-tool executed successfully')
})
it('handles tool execution errors gracefully', async () => {
@@ -181,8 +153,8 @@ describe('mcp.ts', () => {
type: 'function',
function: {
name: 'failing-tool',
arguments: '{"param": "value"}'
}
arguments: '{"param": "value"}',
},
}
const toolError = new Error('Tool execution failed')
@@ -194,10 +166,10 @@ describe('mcp.ts', () => {
tool_call_id: 'call-456',
role: 'tool',
name: 'failing-tool',
content: 'Error: Error: Tool execution failed'
content: 'Error: Error: Tool execution failed',
})
expect(core.warning).toHaveBeenCalledWith(
'Failed to execute GitHub MCP tool failing-tool: Error: Tool execution failed'
'Failed to execute GitHub MCP tool failing-tool: Error: Tool execution failed',
)
})
@@ -207,8 +179,8 @@ describe('mcp.ts', () => {
type: 'function',
function: {
name: 'test-tool',
arguments: 'invalid-json'
}
arguments: 'invalid-json',
},
}
const result = await executeToolCall(mockClient, toolCall)
@@ -217,9 +189,7 @@ describe('mcp.ts', () => {
expect(result.role).toBe('tool')
expect(result.name).toBe('test-tool')
expect(result.content).toContain('Error:')
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining('Failed to execute GitHub MCP tool test-tool:')
)
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('Failed to execute GitHub MCP tool test-tool:'))
})
})
@@ -229,21 +199,21 @@ describe('mcp.ts', () => {
{
id: 'call-1',
type: 'function',
function: { name: 'tool-1', arguments: '{}' }
function: {name: 'tool-1', arguments: '{}'},
},
{
id: 'call-2',
type: 'function',
function: { name: 'tool-2', arguments: '{"param": "value"}' }
}
function: {name: 'tool-2', arguments: '{"param": "value"}'},
},
]
mockCallTool
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Result 1' }]
content: [{type: 'text', text: 'Result 1'}],
})
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Result 2' }]
content: [{type: 'text', text: 'Result 2'}],
})
const results = await executeToolCalls(mockClient, toolCalls)
@@ -266,18 +236,18 @@ describe('mcp.ts', () => {
{
id: 'call-1',
type: 'function',
function: { name: 'tool-1', arguments: '{}' }
function: {name: 'tool-1', arguments: '{}'},
},
{
id: 'call-2',
type: 'function',
function: { name: 'tool-2', arguments: '{}' }
}
function: {name: 'tool-2', arguments: '{}'},
},
]
mockCallTool
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Result 1' }]
content: [{type: 'text', text: 'Result 1'}],
})
.mockRejectedValueOnce(new Error('Tool 2 failed'))
+13 -26
View File
@@ -1,12 +1,7 @@
import { describe, it, expect } from '@jest/globals'
import {describe, it, expect} from 'vitest'
import * as path from 'path'
import { fileURLToPath } from 'url'
import {
parseTemplateVariables,
replaceTemplateVariables,
loadPromptFile,
isPromptYamlFile
} from '../src/prompt'
import {fileURLToPath} from 'url'
import {parseTemplateVariables, replaceTemplateVariables, loadPromptFile, isPromptYamlFile} from '../src/prompt'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
@@ -19,7 +14,7 @@ a: hello
b: world
`
const result = parseTemplateVariables(input)
expect(result).toEqual({ a: 'hello', b: 'world' })
expect(result).toEqual({a: 'hello', b: 'world'})
})
it('should parse multiline variables', () => {
@@ -49,14 +44,14 @@ var2: |
describe('replaceTemplateVariables', () => {
it('should replace simple variables', () => {
const text = 'Hello {{name}}, welcome to {{place}}!'
const variables = { name: 'John', place: 'GitHub' }
const variables = {name: 'John', place: 'GitHub'}
const result = replaceTemplateVariables(text, variables)
expect(result).toBe('Hello John, welcome to GitHub!')
})
it('should leave unreplaced variables as is', () => {
const text = 'Hello {{name}}, welcome to {{unknown}}!'
const variables = { name: 'John' }
const variables = {name: 'John'}
const result = replaceTemplateVariables(text, variables)
expect(result).toBe('Hello John, welcome to {{unknown}}!')
})
@@ -90,31 +85,25 @@ var2: |
describe('loadPromptFile', () => {
it('should load simple prompt file', () => {
const filePath = path.join(
__dirname,
'../__fixtures__/prompts/simple.prompt.yml'
)
const variables = { a: 'cats', b: 'dogs' }
const filePath = path.join(__dirname, '../__fixtures__/prompts/simple.prompt.yml')
const variables = {a: 'cats', b: 'dogs'}
const result = loadPromptFile(filePath, variables)
expect(result.messages).toHaveLength(2)
expect(result.messages[0]).toEqual({
role: 'system',
content: 'Be as concise as possible'
content: 'Be as concise as possible',
})
expect(result.messages[1]).toEqual({
role: 'user',
content: 'Compare cats and dogs, please'
content: 'Compare cats and dogs, please',
})
expect(result.model).toBe('openai/gpt-4o')
})
it('should load JSON schema prompt file', () => {
const filePath = path.join(
__dirname,
'../__fixtures__/prompts/json-schema.prompt.yml'
)
const variables = { animal: 'dog' }
const filePath = path.join(__dirname, '../__fixtures__/prompts/json-schema.prompt.yml')
const variables = {animal: 'dog'}
const result = loadPromptFile(filePath, variables)
expect(result.messages).toHaveLength(2)
@@ -125,9 +114,7 @@ var2: |
})
it('should throw error for non-existent file', () => {
expect(() => loadPromptFile('non-existent.prompt.yml')).toThrow(
'Prompt file not found'
)
expect(() => loadPromptFile('non-existent.prompt.yml')).toThrow('Prompt file not found')
})
})
})
+5 -2
View File
@@ -14,8 +14,7 @@ inputs:
required: false
default: ''
prompt-file:
description:
Path to a file containing the prompt (supports .txt and .prompt.yml
description: Path to a file containing the prompt (supports .txt and .prompt.yml
formats)
required: false
default: ''
@@ -51,6 +50,10 @@ inputs:
description: Enable Model Context Protocol integration with GitHub tools
required: false
default: 'false'
github-mcp-token:
description: The token to use for GitHub MCP server (defaults to GITHUB_TOKEN if not specified)
required: false
default: ''
# Define your outputs here.
outputs:
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" role="img" aria-label="Coverage: 84.21%"><title>Coverage: 84.21%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#dfb317"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">84.21%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">84.21%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Generated Vendored
+32 -31
View File
@@ -41878,14 +41878,14 @@ async function connectToGitHubMCP(token) {
requestInit: {
headers: {
Authorization: `Bearer ${token}`,
'X-MCP-Readonly': 'true'
}
}
'X-MCP-Readonly': 'true',
},
},
});
const client = new Client({
name: 'ai-inference-action',
version: '1.0.0',
transport
transport,
});
try {
await client.connect(transport);
@@ -41898,13 +41898,13 @@ async function connectToGitHubMCP(token) {
const toolsResponse = await client.listTools();
coreExports.info(`Retrieved ${toolsResponse.tools?.length || 0} tools from GitHub MCP server`);
// Map GitHub MCP tools → Azure AI Inference tool definitions
const tools = (toolsResponse.tools || []).map((t) => ({
const tools = (toolsResponse.tools || []).map(t => ({
type: 'function',
function: {
name: t.name,
description: t.description,
parameters: t.inputSchema
}
parameters: t.inputSchema,
},
}));
coreExports.info(`Mapped ${tools.length} GitHub MCP tools for Azure AI Inference`);
return { client, tools };
@@ -41918,14 +41918,14 @@ async function executeToolCall(githubMcpClient, toolCall) {
const args = JSON.parse(toolCall.function.arguments);
const result = await githubMcpClient.callTool({
name: toolCall.function.name,
arguments: args
arguments: args,
});
coreExports.info(`GitHub MCP tool ${toolCall.function.name} executed successfully`);
return {
tool_call_id: toolCall.id,
role: 'tool',
name: toolCall.function.name,
content: JSON.stringify(result.content)
content: JSON.stringify(result.content),
};
}
catch (toolError) {
@@ -41934,7 +41934,7 @@ async function executeToolCall(githubMcpClient, toolCall) {
tool_call_id: toolCall.id,
role: 'tool',
name: toolCall.function.name,
content: `Error: ${toolError}`
content: `Error: ${toolError}`,
};
}
}
@@ -49083,9 +49083,7 @@ function handleUnexpectedResponse(response) {
}
// Handle other error cases
throw new Error(`AI service returned error response (status: ${response.status})${errorCodeMsg}: ` +
(typeof response.body === 'string'
? response.body
: JSON.stringify(response.body)));
(typeof response.body === 'string' ? response.body : JSON.stringify(response.body)));
}
/**
* Build messages array from either prompt config or legacy format
@@ -49093,9 +49091,9 @@ function handleUnexpectedResponse(response) {
function buildMessages(promptConfig, systemPrompt, prompt) {
if (promptConfig?.messages && promptConfig.messages.length > 0) {
// Use new message format
return promptConfig.messages.map((msg) => ({
return promptConfig.messages.map(msg => ({
role: msg.role,
content: msg.content
content: msg.content,
}));
}
else {
@@ -49103,9 +49101,9 @@ function buildMessages(promptConfig, systemPrompt, prompt) {
return [
{
role: 'system',
content: systemPrompt || 'You are a helpful assistant'
content: systemPrompt || 'You are a helpful assistant',
},
{ role: 'user', content: prompt || '' }
{ role: 'user', content: prompt || '' },
];
}
}
@@ -49113,13 +49111,12 @@ function buildMessages(promptConfig, systemPrompt, prompt) {
* Build response format object for API from prompt config
*/
function buildResponseFormat(promptConfig) {
if (promptConfig?.responseFormat === 'json_schema' &&
promptConfig.jsonSchema) {
if (promptConfig?.responseFormat === 'json_schema' && promptConfig.jsonSchema) {
try {
const schema = JSON.parse(promptConfig.jsonSchema);
return {
type: 'json_schema',
json_schema: schema
json_schema: schema,
};
}
catch (error) {
@@ -49140,7 +49137,7 @@ function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, ma
maxTokens,
endpoint,
token,
responseFormat
responseFormat,
};
}
@@ -49150,19 +49147,19 @@ function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, ma
async function simpleInference(request) {
coreExports.info('Running simple inference without tools');
const client = createClient(request.endpoint, new AzureKeyCredential(request.token), {
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' }
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' },
});
const requestBody = {
messages: request.messages,
max_tokens: request.maxTokens,
model: request.modelName
model: request.modelName,
};
// Add response format if specified
if (request.responseFormat) {
requestBody.response_format = request.responseFormat;
}
const response = await client.path('/chat/completions').post({
body: requestBody
body: requestBody,
});
if (isUnexpected(response)) {
handleUnexpectedResponse(response);
@@ -49177,7 +49174,7 @@ async function simpleInference(request) {
async function mcpInference(request, githubMcpClient) {
coreExports.info('Running GitHub MCP inference with tools');
const client = createClient(request.endpoint, new AzureKeyCredential(request.token), {
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' }
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' },
});
// Start with the pre-processed messages
const messages = [...request.messages];
@@ -49190,14 +49187,14 @@ async function mcpInference(request, githubMcpClient) {
messages: messages,
max_tokens: request.maxTokens,
model: request.modelName,
tools: githubMcpClient.tools
tools: githubMcpClient.tools,
};
// Add response format if specified (only on first iteration to avoid conflicts)
if (iterationCount === 1 && request.responseFormat) {
requestBody.response_format = request.responseFormat;
}
const response = await client.path('/chat/completions').post({
body: requestBody
body: requestBody,
});
if (isUnexpected(response)) {
handleUnexpectedResponse(response);
@@ -49209,7 +49206,7 @@ async function mcpInference(request, githubMcpClient) {
messages.push({
role: 'assistant',
content: modelResponse || '',
...(toolCalls && { tool_calls: toolCalls })
...(toolCalls && { tool_calls: toolCalls }),
});
if (!toolCalls || toolCalls.length === 0) {
coreExports.info('No tool calls requested, ending GitHub MCP inference loop');
@@ -49227,7 +49224,7 @@ async function mcpInference(request, githubMcpClient) {
const lastAssistantMessage = messages
.slice()
.reverse()
.find((msg) => msg.role === 'assistant');
.find(msg => msg.role === 'assistant');
return lastAssistantMessage?.content || null;
}
@@ -52133,13 +52130,15 @@ async function run() {
if (token === undefined) {
throw new Error('GITHUB_TOKEN is not set');
}
// Get GitHub MCP token (use dedicated token if provided, otherwise fall back to main token)
const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
const endpoint = coreExports.getInput('endpoint');
// Build the inference request with pre-processed messages and response format
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, maxTokens, endpoint, token);
const enableMcp = coreExports.getBooleanInput('enable-github-mcp') || false;
let modelResponse = null;
if (enableMcp) {
const mcpClient = await connectToGitHubMCP(inferenceRequest.token);
const mcpClient = await connectToGitHubMCP(githubMcpToken);
if (mcpClient) {
modelResponse = await mcpInference(inferenceRequest, mcpClient);
}
@@ -52163,8 +52162,10 @@ async function run() {
coreExports.setFailed(error.message);
}
else {
coreExports.setFailed('An unexpected error occurred');
coreExports.setFailed(`An unexpected error occurred: ${JSON.stringify(error, null, 2)}`);
}
// Force exit to prevent hanging on open connections
process.exit(1);
}
}
function tempDir() {
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+15 -22
View File
@@ -1,50 +1,43 @@
// See: https://eslint.org/docs/latest/use/configure/configuration-files
import { fixupPluginRules } from '@eslint/compat'
import { FlatCompat } from '@eslint/eslintrc'
import {FlatCompat} from '@eslint/eslintrc'
import js from '@eslint/js'
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import _import from 'eslint-plugin-import'
import jest from 'eslint-plugin-jest'
import prettier from 'eslint-plugin-prettier'
import globals from 'globals'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import {fileURLToPath} from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
allConfig: js.configs.all,
})
export default [
{
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules']
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules'],
},
...compat.extends(
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:prettier/recommended'
'plugin:prettier/recommended',
),
{
plugins: {
import: fixupPluginRules(_import),
jest,
prettier,
'@typescript-eslint': typescriptEslint
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
globals: {
...globals.node,
...globals.jest,
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
SharedArrayBuffer: 'readonly',
},
parser: tsParser,
@@ -53,17 +46,17 @@ export default [
parserOptions: {
project: ['tsconfig.eslint.json'],
tsconfigRootDir: '.'
}
tsconfigRootDir: '.',
},
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: 'tsconfig.eslint.json'
}
}
project: 'tsconfig.eslint.json',
},
},
},
rules: {
@@ -75,7 +68,7 @@ export default [
'no-console': 'off',
'no-shadow': 'off',
'no-unused-vars': 'off',
'prettier/prettier': 'error'
}
}
'prettier/prettier': 'error',
},
},
]
-40
View File
@@ -1,40 +0,0 @@
// See: https://jestjs.io/docs/configuration
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: ['./src/**'],
coverageDirectory: './coverage',
coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
coverageReporters: ['json-summary', 'text', 'lcov'],
// Uncomment the below lines if you would like to enforce a coverage threshold
// for your action. This will fail the build if the coverage is below the
// specified thresholds.
// coverageThreshold: {
// global: {
// branches: 100,
// functions: 100,
// lines: 100,
// statements: 100
// }
// },
extensionsToTreatAsEsm: ['.ts'],
moduleFileExtensions: ['ts', 'js'],
preset: 'ts-jest',
reporters: ['default'],
resolver: 'ts-jest-resolver',
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
tsconfig: 'tsconfig.eslint.json',
useESM: true
}
]
},
verbose: true
}
+602 -4899
View File
File diff suppressed because it is too large Load Diff
+11 -30
View File
@@ -1,21 +1,7 @@
{
"name": "typescript-action",
"description": "GitHub Actions TypeScript template",
"name": "ai-inference",
"version": "1.0.0",
"author": "",
"type": "module",
"private": true,
"homepage": "https://github.com/actions/typescript-action",
"repository": {
"type": "git",
"url": "git+https://github.com/actions/typescript-action.git"
},
"bugs": {
"url": "https://github.com/actions/typescript-action/issues"
},
"keywords": [
"actions"
],
"exports": {
".": "./dist/index.js"
},
@@ -24,23 +10,21 @@
},
"scripts": {
"bundle": "npm run format:write && npm run package",
"ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"coverage": "npx make-coverage-badge --output-path ./badges/coverage.svg",
"format:write": "npx prettier --write .",
"format:check": "npx prettier --check .",
"lint": "npx eslint .",
"local-action": "npx @github/local-action . src/main.ts .env",
"package": "npx rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
"package:watch": "npm run package -- --watch",
"test": "npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package"
"test": "vitest --run",
"test:watch": "vitest --watch",
"all": "npm run format:write && npm run lint && npm run test && npm run package"
},
"license": "MIT",
"prettier": "@github/prettier-config",
"dependencies": {
"@actions/core": "^1.11.1",
"@modelcontextprotocol/sdk": "^1.15.1",
"@rollup/plugin-json": "^6.1.0",
"@types/js-yaml": "^4.0.9",
"js-yaml": "^4.1.0",
"pkce-challenge": "^5.0.0"
},
@@ -49,12 +33,13 @@
"@azure/core-auth": "latest",
"@azure/core-sse": "latest",
"@eslint/compat": "^1.3.0",
"@github/local-action": "^3.2.1",
"@jest/globals": "^30.0.2",
"@github/local-action": "^5.1.0",
"@github/prettier-config": "^0.0.6",
"@rollup/plugin-commonjs": "^28.0.5",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"@types/jest": "^29.5.14",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.15.31",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.32.1",
@@ -62,16 +47,12 @@
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^4.4.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.14.0",
"eslint-plugin-prettier": "^5.4.1",
"jest": "^29.7.0",
"make-coverage-badge": "^1.2.0",
"prettier": "^3.5.3",
"prettier-eslint": "^16.4.2",
"rollup": "^4.43.0",
"ts-jest": "^29.4.0",
"ts-jest-resolver": "^2.0.1",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"vitest": "^3"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "*"
+6 -6
View File
@@ -1,5 +1,5 @@
// See: https://rollupjs.org/introduction/
import { builtinModules } from 'node:module'
import {builtinModules} from 'node:module'
import commonjs from '@rollup/plugin-commonjs'
import nodeResolve from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
@@ -11,7 +11,7 @@ const config = {
esModule: true,
file: 'dist/index.js',
format: 'es',
sourcemap: true
sourcemap: true,
},
external: [...builtinModules, /^node:/],
plugins: [
@@ -19,13 +19,13 @@ const config = {
nodeResolve({
preferBuiltins: true,
browser: false,
exportConditions: ['node']
exportConditions: ['node'],
}),
commonjs({
include: /node_modules/
include: /node_modules/,
}),
json()
]
json(),
],
}
export default config
+20 -33
View File
@@ -1,8 +1,8 @@
import * as core from '@actions/core'
import { GetChatCompletionsDefaultResponse } from '@azure-rest/ai-inference'
import {GetChatCompletionsDefaultResponse} from '@azure-rest/ai-inference'
import * as fs from 'fs'
import { PromptConfig } from './prompt.js'
import { InferenceRequest } from './inference.js'
import {PromptConfig} from './prompt.js'
import {InferenceRequest} from './inference.js'
/**
* Helper function to load content from a file or use fallback input
@@ -11,11 +11,7 @@ import { InferenceRequest } from './inference.js'
* @param defaultValue - Default value to use if neither file nor content is provided
* @returns The loaded content
*/
export function loadContentFromFileOrInput(
filePathInput: string,
contentInput: string,
defaultValue?: string
): string {
export function loadContentFromFileOrInput(filePathInput: string, contentInput: string, defaultValue?: string): string {
const filePath = core.getInput(filePathInput)
const contentString = core.getInput(contentInput)
@@ -38,9 +34,7 @@ export function loadContentFromFileOrInput(
* @param response - The response object from the AI service
* @throws Error with appropriate error message based on response content
*/
export function handleUnexpectedResponse(
response: GetChatCompletionsDefaultResponse
): never {
export function handleUnexpectedResponse(response: GetChatCompletionsDefaultResponse): never {
// Extract x-ms-error-code from headers if available
const errorCode = response.headers['x-ms-error-code']
const errorCodeMsg = errorCode ? ` (error code: ${errorCode})` : ''
@@ -54,16 +48,14 @@ export function handleUnexpectedResponse(
if (!response.body) {
throw new Error(
`Failed to get response from AI service (status: ${response.status})${errorCodeMsg}. ` +
'Please check network connection and endpoint configuration.'
'Please check network connection and endpoint configuration.',
)
}
// Handle other error cases
throw new Error(
`AI service returned error response (status: ${response.status})${errorCodeMsg}: ` +
(typeof response.body === 'string'
? response.body
: JSON.stringify(response.body))
(typeof response.body === 'string' ? response.body : JSON.stringify(response.body)),
)
}
@@ -73,22 +65,22 @@ export function handleUnexpectedResponse(
export function buildMessages(
promptConfig?: PromptConfig,
systemPrompt?: string,
prompt?: string
): Array<{ role: string; content: string }> {
prompt?: string,
): Array<{role: string; content: string}> {
if (promptConfig?.messages && promptConfig.messages.length > 0) {
// Use new message format
return promptConfig.messages.map((msg) => ({
return promptConfig.messages.map(msg => ({
role: msg.role,
content: msg.content
content: msg.content,
}))
} else {
// Use legacy format
return [
{
role: 'system',
content: systemPrompt || 'You are a helpful assistant'
content: systemPrompt || 'You are a helpful assistant',
},
{ role: 'user', content: prompt || '' }
{role: 'user', content: prompt || ''},
]
}
}
@@ -97,22 +89,17 @@ export function buildMessages(
* Build response format object for API from prompt config
*/
export function buildResponseFormat(
promptConfig?: PromptConfig
): { type: 'json_schema'; json_schema: unknown } | undefined {
if (
promptConfig?.responseFormat === 'json_schema' &&
promptConfig.jsonSchema
) {
promptConfig?: PromptConfig,
): {type: 'json_schema'; json_schema: unknown} | undefined {
if (promptConfig?.responseFormat === 'json_schema' && promptConfig.jsonSchema) {
try {
const schema = JSON.parse(promptConfig.jsonSchema)
return {
type: 'json_schema',
json_schema: schema
json_schema: schema,
}
} catch (error) {
throw new Error(
`Invalid JSON schema: ${error instanceof Error ? error.message : 'Unknown error'}`
)
throw new Error(`Invalid JSON schema: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
return undefined
@@ -128,7 +115,7 @@ export function buildInferenceRequest(
modelName: string,
maxTokens: number,
endpoint: string,
token: string
token: string,
): InferenceRequest {
const messages = buildMessages(promptConfig, systemPrompt, prompt)
const responseFormat = buildResponseFormat(promptConfig)
@@ -139,6 +126,6 @@ export function buildInferenceRequest(
maxTokens,
endpoint,
token,
responseFormat
responseFormat,
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
* The entrypoint for the action. This file simply imports and runs the action's
* main logic.
*/
import { run } from './main.js'
import {run} from './main.js'
/* istanbul ignore next */
run()
+23 -38
View File
@@ -1,8 +1,8 @@
import * as core from '@actions/core'
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'
import { AzureKeyCredential } from '@azure/core-auth'
import { GitHubMCPClient, executeToolCalls, MCPTool, ToolCall } from './mcp.js'
import { handleUnexpectedResponse } from './helpers.js'
import ModelClient, {isUnexpected} from '@azure-rest/ai-inference'
import {AzureKeyCredential} from '@azure/core-auth'
import {GitHubMCPClient, executeToolCalls, MCPTool, ToolCall} from './mcp.js'
import {handleUnexpectedResponse} from './helpers.js'
interface ChatMessage {
role: string
@@ -14,17 +14,17 @@ interface ChatCompletionsRequestBody {
messages: ChatMessage[]
max_tokens: number
model: string
response_format?: { type: 'json_schema'; json_schema: unknown }
response_format?: {type: 'json_schema'; json_schema: unknown}
tools?: MCPTool[]
}
export interface InferenceRequest {
messages: Array<{ role: string; content: string }>
messages: Array<{role: string; content: string}>
modelName: string
maxTokens: number
endpoint: string
token: string
responseFormat?: { type: 'json_schema'; json_schema: unknown } // Processed response format for the API
responseFormat?: {type: 'json_schema'; json_schema: unknown} // Processed response format for the API
}
export interface InferenceResponse {
@@ -42,23 +42,17 @@ export interface InferenceResponse {
/**
* Simple one-shot inference without tools
*/
export async function simpleInference(
request: InferenceRequest
): Promise<string | null> {
export async function simpleInference(request: InferenceRequest): Promise<string | null> {
core.info('Running simple inference without tools')
const client = ModelClient(
request.endpoint,
new AzureKeyCredential(request.token),
{
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' }
}
)
const client = ModelClient(request.endpoint, new AzureKeyCredential(request.token), {
userAgentOptions: {userAgentPrefix: 'github-actions-ai-inference'},
})
const requestBody: ChatCompletionsRequestBody = {
messages: request.messages,
max_tokens: request.maxTokens,
model: request.modelName
model: request.modelName,
}
// Add response format if specified
@@ -67,7 +61,7 @@ export async function simpleInference(
}
const response = await client.path('/chat/completions').post({
body: requestBody
body: requestBody,
})
if (isUnexpected(response)) {
@@ -85,17 +79,13 @@ export async function simpleInference(
*/
export async function mcpInference(
request: InferenceRequest,
githubMcpClient: GitHubMCPClient
githubMcpClient: GitHubMCPClient,
): Promise<string | null> {
core.info('Running GitHub MCP inference with tools')
const client = ModelClient(
request.endpoint,
new AzureKeyCredential(request.token),
{
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' }
}
)
const client = ModelClient(request.endpoint, new AzureKeyCredential(request.token), {
userAgentOptions: {userAgentPrefix: 'github-actions-ai-inference'},
})
// Start with the pre-processed messages
const messages: ChatMessage[] = [...request.messages]
@@ -111,7 +101,7 @@ export async function mcpInference(
messages: messages,
max_tokens: request.maxTokens,
model: request.modelName,
tools: githubMcpClient.tools
tools: githubMcpClient.tools,
}
// Add response format if specified (only on first iteration to avoid conflicts)
@@ -120,7 +110,7 @@ export async function mcpInference(
}
const response = await client.path('/chat/completions').post({
body: requestBody
body: requestBody,
})
if (isUnexpected(response)) {
@@ -136,7 +126,7 @@ export async function mcpInference(
messages.push({
role: 'assistant',
content: modelResponse || '',
...(toolCalls && { tool_calls: toolCalls })
...(toolCalls && {tool_calls: toolCalls}),
})
if (!toolCalls || toolCalls.length === 0) {
@@ -147,10 +137,7 @@ export async function mcpInference(
core.info(`Model requested ${toolCalls.length} tool calls`)
// Execute all tool calls via GitHub MCP
const toolResults = await executeToolCalls(
githubMcpClient.client,
toolCalls
)
const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls)
// Add tool results to the conversation
messages.push(...toolResults)
@@ -158,15 +145,13 @@ export async function mcpInference(
core.info('Tool results added, continuing conversation...')
}
core.warning(
`GitHub MCP inference loop exceeded maximum iterations (${maxIterations})`
)
core.warning(`GitHub MCP inference loop exceeded maximum iterations (${maxIterations})`)
// Return the last assistant message content
const lastAssistantMessage = messages
.slice()
.reverse()
.find((msg) => msg.role === 'assistant')
.find(msg => msg.role === 'assistant')
return lastAssistantMessage?.content || null
}
+14 -17
View File
@@ -2,15 +2,10 @@ import * as core from '@actions/core'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import { connectToGitHubMCP } from './mcp.js'
import { simpleInference, mcpInference } from './inference.js'
import { loadContentFromFileOrInput, buildInferenceRequest } from './helpers.js'
import {
loadPromptFile,
parseTemplateVariables,
isPromptYamlFile,
PromptConfig
} from './prompt.js'
import {connectToGitHubMCP} from './mcp.js'
import {simpleInference, mcpInference} from './inference.js'
import {loadContentFromFileOrInput, buildInferenceRequest} from './helpers.js'
import {loadPromptFile, parseTemplateVariables, isPromptYamlFile, PromptConfig} from './prompt.js'
const RESPONSE_FILE = 'modelResponse.txt'
@@ -42,11 +37,7 @@ export async function run(): Promise<void> {
core.info('Using legacy prompt format')
prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
systemPrompt = loadContentFromFileOrInput(
'system-prompt-file',
'system-prompt',
'You are a helpful assistant'
)
systemPrompt = loadContentFromFileOrInput('system-prompt-file', 'system-prompt', 'You are a helpful assistant')
}
// Get common parameters
@@ -58,6 +49,9 @@ export async function run(): Promise<void> {
throw new Error('GITHUB_TOKEN is not set')
}
// Get GitHub MCP token (use dedicated token if provided, otherwise fall back to main token)
const githubMcpToken = core.getInput('github-mcp-token') || token
const endpoint = core.getInput('endpoint')
// Build the inference request with pre-processed messages and response format
@@ -68,7 +62,7 @@ export async function run(): Promise<void> {
modelName,
maxTokens,
endpoint,
token
token,
)
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
@@ -76,7 +70,7 @@ export async function run(): Promise<void> {
let modelResponse: string | null = null
if (enableMcp) {
const mcpClient = await connectToGitHubMCP(inferenceRequest.token)
const mcpClient = await connectToGitHubMCP(githubMcpToken)
if (mcpClient) {
modelResponse = await mcpInference(inferenceRequest, mcpClient)
@@ -100,8 +94,11 @@ export async function run(): Promise<void> {
if (error instanceof Error) {
core.setFailed(error.message)
} else {
core.setFailed('An unexpected error occurred')
core.setFailed(`An unexpected error occurred: ${JSON.stringify(error, null, 2)}`)
}
// Force exit to prevent hanging on open connections
process.exit(1)
}
}
+19 -33
View File
@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import {Client} from '@modelcontextprotocol/sdk/client/index.js'
import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js'
export interface ToolResult {
tool_call_id: string
@@ -35,9 +35,7 @@ export interface GitHubMCPClient {
/**
* Connect to the GitHub MCP server and retrieve available tools
*/
export async function connectToGitHubMCP(
token: string
): Promise<GitHubMCPClient | null> {
export async function connectToGitHubMCP(token: string): Promise<GitHubMCPClient | null> {
const githubMcpUrl = 'https://api.githubcopilot.com/mcp/'
core.info('Connecting to GitHub MCP server...')
@@ -46,15 +44,15 @@ export async function connectToGitHubMCP(
requestInit: {
headers: {
Authorization: `Bearer ${token}`,
'X-MCP-Readonly': 'true'
}
}
'X-MCP-Readonly': 'true',
},
},
})
const client = new Client({
name: 'ai-inference-action',
version: '1.0.0',
transport
transport,
})
try {
@@ -67,42 +65,35 @@ export async function connectToGitHubMCP(
core.info('Successfully connected to GitHub MCP server')
const toolsResponse = await client.listTools()
core.info(
`Retrieved ${toolsResponse.tools?.length || 0} tools from GitHub MCP server`
)
core.info(`Retrieved ${toolsResponse.tools?.length || 0} tools from GitHub MCP server`)
// Map GitHub MCP tools → Azure AI Inference tool definitions
const tools = (toolsResponse.tools || []).map((t) => ({
const tools = (toolsResponse.tools || []).map(t => ({
type: 'function' as const,
function: {
name: t.name,
description: t.description,
parameters: t.inputSchema
}
parameters: t.inputSchema,
},
}))
core.info(`Mapped ${tools.length} GitHub MCP tools for Azure AI Inference`)
return { client, tools }
return {client, tools}
}
/**
* Execute a single tool call via GitHub MCP
*/
export async function executeToolCall(
githubMcpClient: Client,
toolCall: ToolCall
): Promise<ToolResult> {
core.info(
`Executing GitHub MCP tool: ${toolCall.function.name} with args: ${toolCall.function.arguments}`
)
export async function executeToolCall(githubMcpClient: Client, toolCall: ToolCall): Promise<ToolResult> {
core.info(`Executing GitHub MCP tool: ${toolCall.function.name} with args: ${toolCall.function.arguments}`)
try {
const args = JSON.parse(toolCall.function.arguments)
const result = await githubMcpClient.callTool({
name: toolCall.function.name,
arguments: args
arguments: args,
})
core.info(`GitHub MCP tool ${toolCall.function.name} executed successfully`)
@@ -111,18 +102,16 @@ export async function executeToolCall(
tool_call_id: toolCall.id,
role: 'tool',
name: toolCall.function.name,
content: JSON.stringify(result.content)
content: JSON.stringify(result.content),
}
} catch (toolError) {
core.warning(
`Failed to execute GitHub MCP tool ${toolCall.function.name}: ${toolError}`
)
core.warning(`Failed to execute GitHub MCP tool ${toolCall.function.name}: ${toolError}`)
return {
tool_call_id: toolCall.id,
role: 'tool',
name: toolCall.function.name,
content: `Error: ${toolError}`
content: `Error: ${toolError}`,
}
}
}
@@ -130,10 +119,7 @@ export async function executeToolCall(
/**
* Execute all tool calls from a response via GitHub MCP
*/
export async function executeToolCalls(
githubMcpClient: Client,
toolCalls: ToolCall[]
): Promise<ToolResult[]> {
export async function executeToolCalls(githubMcpClient: Client, toolCalls: ToolCall[]): Promise<ToolResult[]> {
const toolResults: ToolResult[] = []
for (const toolCall of toolCalls) {
+7 -24
View File
@@ -33,26 +33,19 @@ export function parseTemplateVariables(input: string): TemplateVariables {
}
return parsed
} catch (error) {
throw new Error(
`Failed to parse template variables: ${error instanceof Error ? error.message : 'Unknown error'}`
)
throw new Error(`Failed to parse template variables: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Replace template variables in text using {{variable}} syntax
*/
export function replaceTemplateVariables(
text: string,
variables: TemplateVariables
): string {
export function replaceTemplateVariables(text: string, variables: TemplateVariables): string {
return text.replace(/\{\{([\w.-]+)\}\}/g, (match, variableName) => {
if (variableName in variables) {
return variables[variableName]
}
core.warning(
`Template variable '${variableName}' not found in input variables`
)
core.warning(`Template variable '${variableName}' not found in input variables`)
return match // Return the original placeholder if variable not found
})
}
@@ -60,10 +53,7 @@ export function replaceTemplateVariables(
/**
* Load and parse a prompt YAML file with template variable substitution
*/
export function loadPromptFile(
filePath: string,
templateVariables: TemplateVariables = {}
): PromptConfig {
export function loadPromptFile(filePath: string, templateVariables: TemplateVariables = {}): PromptConfig {
if (!fs.existsSync(filePath)) {
throw new Error(`Prompt file not found: ${filePath}`)
}
@@ -71,10 +61,7 @@ export function loadPromptFile(
const fileContent = fs.readFileSync(filePath, 'utf-8')
// Apply template variable substitution
const processedContent = replaceTemplateVariables(
fileContent,
templateVariables
)
const processedContent = replaceTemplateVariables(fileContent, templateVariables)
try {
const config = yaml.load(processedContent) as PromptConfig
@@ -86,9 +73,7 @@ export function loadPromptFile(
// Validate messages
for (const message of config.messages) {
if (!message.role || !message.content) {
throw new Error(
'Each message must have "role" and "content" properties'
)
throw new Error('Each message must have "role" and "content" properties')
}
if (!['system', 'user', 'assistant'].includes(message.role)) {
throw new Error(`Invalid message role: ${message.role}`)
@@ -97,9 +82,7 @@ export function loadPromptFile(
return config
} catch (error) {
throw new Error(
`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`
)
throw new Error(`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
+1 -8
View File
@@ -6,12 +6,5 @@
"noEmit": true
},
"exclude": ["dist", "node_modules"],
"include": [
"__fixtures__",
"__tests__",
"src",
"eslint.config.mjs",
"jest.config.js",
"rollup.config.ts"
]
"include": ["__fixtures__", "__tests__", "src", "eslint.config.mjs", "rollup.config.ts"]
}