Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e29335e7e1 | |||
| defb8d080a | |||
| c6151931b9 | |||
| ce5e368f40 | |||
| 884d4b5d0e | |||
| f54fee0f72 | |||
| e8b436ef61 | |||
| 0e99594b70 | |||
| bd669b352d | |||
| 7437fdf535 | |||
| ee8e2213b0 | |||
| 15175a4780 | |||
| ee64d91eac | |||
| 91bf3cfc28 | |||
| 03559ade48 | |||
| 15730d40fc | |||
| 295869bef1 | |||
| 2fe1353f7a |
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,86 @@
|
||||
name: "Create Coder Task"
|
||||
description: "Create a Coder task for a GitHub user, with support for issue commenting"
|
||||
|
||||
inputs:
|
||||
# Required: Coder configuration
|
||||
coder-url:
|
||||
description: "Coder deployment URL"
|
||||
required: true
|
||||
|
||||
coder-token:
|
||||
description: "Coder session token for authentication"
|
||||
required: true
|
||||
|
||||
# Required: Task configuration
|
||||
template-name:
|
||||
description: "Coder template to use for workspace"
|
||||
required: true
|
||||
|
||||
task-prompt:
|
||||
description: "Prompt/instructions to send to the task"
|
||||
required: true
|
||||
|
||||
# Optional: User identification
|
||||
github-user-id:
|
||||
description: "GitHub user ID (defaults to event sender)"
|
||||
required: false
|
||||
|
||||
github-username:
|
||||
description: "GitHub username (defaults to event sender)"
|
||||
required: false
|
||||
|
||||
# Optional: Task configuration
|
||||
template-preset:
|
||||
description: "Template preset to use"
|
||||
required: false
|
||||
default: "Default"
|
||||
|
||||
task-name-prefix:
|
||||
description: "Prefix for task name"
|
||||
required: false
|
||||
default: "task"
|
||||
|
||||
task-name:
|
||||
description: "Full task name (overrides auto-generation)"
|
||||
required: false
|
||||
|
||||
organization:
|
||||
description: "Coder organization name"
|
||||
required: false
|
||||
default: "coder"
|
||||
|
||||
# Optional: Issue integration
|
||||
issue-url:
|
||||
description: "GitHub issue URL to comment on"
|
||||
required: false
|
||||
|
||||
comment-on-issue:
|
||||
description: "Whether to comment on the issue"
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
coder-web-url:
|
||||
description: "Coder web UI URL for task links (defaults to coder-url)"
|
||||
required: false
|
||||
|
||||
# GitHub token for API operations
|
||||
github-token:
|
||||
description: "GitHub token for commenting on issues"
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
coder-username:
|
||||
description: "The Coder username resolved from GitHub user"
|
||||
|
||||
task-name:
|
||||
description: "The full task name (username/task-name)"
|
||||
|
||||
task-url:
|
||||
description: "The URL to view the task in Coder"
|
||||
|
||||
task-exists:
|
||||
description: "Whether the task already existed (true/false)"
|
||||
|
||||
runs:
|
||||
using: "node20"
|
||||
main: "dist/index.js"
|
||||
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "coder-task-action",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"zod": "^3.24.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="],
|
||||
|
||||
"@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="],
|
||||
|
||||
"@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="],
|
||||
|
||||
"@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
||||
|
||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
|
||||
|
||||
"@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="],
|
||||
|
||||
"@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="],
|
||||
|
||||
"@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
|
||||
|
||||
"@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="],
|
||||
|
||||
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@5.3.1", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="],
|
||||
|
||||
"@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="],
|
||||
|
||||
"@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="],
|
||||
|
||||
"@octokit/rest": ["@octokit/rest@21.1.1", "", { "dependencies": { "@octokit/core": "^6.1.4", "@octokit/plugin-paginate-rest": "^11.4.2", "@octokit/plugin-request-log": "^5.3.1", "@octokit/plugin-rest-endpoint-methods": "^13.3.0" } }, "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg=="],
|
||||
|
||||
"@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||
|
||||
"before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||
|
||||
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||
|
||||
"universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core": ["@octokit/core@6.1.6", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@octokit/rest/@octokit/core": ["@octokit/core@6.1.6", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA=="],
|
||||
|
||||
"@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@11.6.0", "", { "dependencies": { "@octokit/types": "^13.10.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw=="],
|
||||
|
||||
"@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@13.5.0", "", { "dependencies": { "@octokit/types": "^13.10.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@9.2.4", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@9.2.4", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "coder-task-action",
|
||||
"version": "1.0.0",
|
||||
"description": "GitHub Action to create and manage Coder tasks",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "bun build src/index.ts --outfile dist/index.js --target node",
|
||||
"dev": "bun run --watch src/index.ts",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome format .",
|
||||
"lint": "biome lint --error-on-warnings .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
import { describe, expect, test, beforeEach } from "bun:test";
|
||||
import { CoderTaskAction } from "./action";
|
||||
import type { Octokit } from "./action";
|
||||
import {
|
||||
MockCoderClient,
|
||||
createMockOctokit,
|
||||
createMockInputs,
|
||||
mockUser,
|
||||
mockTask,
|
||||
mockTemplate,
|
||||
} from "./test-helpers";
|
||||
|
||||
describe("CoderTaskAction", () => {
|
||||
let coderClient: MockCoderClient;
|
||||
let octokit: ReturnType<typeof createMockOctokit>;
|
||||
|
||||
beforeEach(() => {
|
||||
coderClient = new MockCoderClient();
|
||||
octokit = createMockOctokit();
|
||||
});
|
||||
|
||||
describe("parseGithubIssueUrl", () => {
|
||||
test("parses valid GitHub issue URL", () => {
|
||||
const inputs = createMockInputs({
|
||||
githubIssueURL: "https://github.com/owner/repo/issues/123",
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
const result = (
|
||||
action as unknown as CoderTaskAction
|
||||
).parseGithubIssueURL();
|
||||
|
||||
expect(result).toEqual({
|
||||
githubOrg: "owner",
|
||||
githubRepo: "repo",
|
||||
githubIssueNumber: 123,
|
||||
});
|
||||
});
|
||||
|
||||
test("throws when no issue URL provided", () => {
|
||||
const inputs = createMockInputs({ githubIssueURL: undefined });
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
const result = (
|
||||
action as unknown as CoderTaskAction
|
||||
).parseGithubIssueURL();
|
||||
|
||||
expect(result).toThrowError("Missing issue URL");
|
||||
});
|
||||
|
||||
test("throws for invalid URL format", () => {
|
||||
const inputs = createMockInputs({ githubIssueURL: "not-a-url" });
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
const result = (
|
||||
action as unknown as CoderTaskAction
|
||||
).parseGithubIssueURL();
|
||||
|
||||
expect(result).toThrowError("Invalid issue URL: not-a-url");
|
||||
});
|
||||
|
||||
test("handled non-github.com URL", () => {
|
||||
const inputs = createMockInputs({
|
||||
githubIssueURL: "https://code.acme.com/owner/repo/issues/123",
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
const result = (
|
||||
action as unknown as CoderTaskAction
|
||||
).parseGithubIssueURL();
|
||||
|
||||
expect(result).toEqual({
|
||||
githubOrg: "owner",
|
||||
githubRepo: "repo",
|
||||
githubIssueNumber: 123,
|
||||
});
|
||||
});
|
||||
|
||||
test("handles URL with trailing junk", () => {
|
||||
const inputs = createMockInputs({
|
||||
githubIssueURL:
|
||||
"https://github.com/owner/repo/issues/123/?param=value#anchor",
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
const result = (
|
||||
action as unknown as CoderTaskAction
|
||||
).parseGithubIssueURL();
|
||||
|
||||
// Should still parse correctly
|
||||
expect(result).toEqual({
|
||||
githubOrg: "owner",
|
||||
githubRepo: "repo",
|
||||
githubIssueNumber: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateTaskUrl", () => {
|
||||
test("generates correct task URL", () => {
|
||||
const inputs = createMockInputs();
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
const result = (action as unknown as CoderTaskAction).generateTaskUrl(
|
||||
"testuser",
|
||||
"task-123",
|
||||
);
|
||||
|
||||
expect(result).toBe("https://coder.test/tasks/testuser/task-123");
|
||||
});
|
||||
|
||||
test("handles URL with trailing junk", () => {
|
||||
const inputs = createMockInputs({
|
||||
coderURL: "https://coder.test/?param=value#anchor",
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
const result = (action as unknown as CoderTaskAction).generateTaskUrl(
|
||||
"testuser",
|
||||
"task-123",
|
||||
);
|
||||
|
||||
// Should not have double slash
|
||||
expect(result).toBe("https://coder.test//tasks/testuser/task-123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("commentOnIssue", () => {
|
||||
describe("Success Cases", () => {
|
||||
test("creates new comment when none exists", async () => {
|
||||
octokit.rest.issues.listComments.mockResolvedValue({
|
||||
data: [],
|
||||
} as ReturnType<typeof octokit.rest.issues.listComments>);
|
||||
octokit.rest.issues.createComment.mockResolvedValue(
|
||||
{} as ReturnType<typeof octokit.rest.issues.createComment>,
|
||||
);
|
||||
|
||||
const inputs = createMockInputs();
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
await (action as unknown as CoderTaskAction).commentOnIssue(
|
||||
"https://coder.test/tasks/testuser/task-123",
|
||||
"owner",
|
||||
"repo",
|
||||
123,
|
||||
);
|
||||
|
||||
expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
issue_number: 123,
|
||||
body: "Task created: https://coder.test/tasks/testuser/task-123",
|
||||
});
|
||||
});
|
||||
|
||||
test("updates existing Task created comment", async () => {
|
||||
octokit.rest.issues.listComments.mockResolvedValue({
|
||||
data: [
|
||||
{ id: 1, body: "Task created: old-url" },
|
||||
{ id: 2, body: "Other comment" },
|
||||
{ id: 3, body: "Task created: another-old-url" },
|
||||
],
|
||||
} as ReturnType<typeof octokit.rest.issues.listComments>);
|
||||
octokit.rest.issues.updateComment.mockResolvedValue(
|
||||
{} as ReturnType<typeof octokit.rest.issues.updateComment>,
|
||||
);
|
||||
|
||||
const inputs = createMockInputs();
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
await (action as unknown as CoderTaskAction).commentOnIssue(
|
||||
"https://coder.test/tasks/testuser/task-123",
|
||||
"owner",
|
||||
"repo",
|
||||
123,
|
||||
);
|
||||
|
||||
// Should update the last "Task created:" comment
|
||||
expect(octokit.rest.issues.updateComment).toHaveBeenCalledWith({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
comment_id: 3,
|
||||
body: "Task created: https://coder.test/tasks/testuser/task-123",
|
||||
});
|
||||
});
|
||||
|
||||
test("parses owner/repo/issue from URL correctly", async () => {
|
||||
octokit.rest.issues.listComments.mockResolvedValue({
|
||||
data: [],
|
||||
} as ReturnType<typeof octokit.rest.issues.listComments>);
|
||||
octokit.rest.issues.createComment.mockResolvedValue(
|
||||
{} as ReturnType<typeof octokit.rest.issues.createComment>,
|
||||
);
|
||||
|
||||
const inputs = createMockInputs();
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
await (action as unknown as CoderTaskAction).commentOnIssue(
|
||||
"https://coder.test/tasks/testuser/task-123",
|
||||
"different-owner",
|
||||
"different-repo",
|
||||
456,
|
||||
);
|
||||
|
||||
expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({
|
||||
owner: "different-owner",
|
||||
repo: "different-repo",
|
||||
issue_number: 456,
|
||||
body: "Task created: https://coder.test/tasks/testuser/task-123",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Cases", () => {
|
||||
test("warns but doesn't fail on GitHub API error", async () => {
|
||||
octokit.rest.issues.listComments.mockRejectedValue(
|
||||
new Error("API Error"),
|
||||
);
|
||||
|
||||
const inputs = createMockInputs();
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
// Should not throw
|
||||
expect(
|
||||
(action as unknown as CoderTaskAction).commentOnIssue(
|
||||
"https://coder.test/tasks/testuser/task-123",
|
||||
"owner",
|
||||
"repo",
|
||||
123,
|
||||
),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("warns but doesn't fail on permission error", async () => {
|
||||
octokit.rest.issues.listComments.mockResolvedValue({
|
||||
data: [],
|
||||
} as ReturnType<typeof octokit.rest.issues.listComments>);
|
||||
octokit.rest.issues.createComment.mockRejectedValue(
|
||||
new Error("Permission denied"),
|
||||
);
|
||||
|
||||
const inputs = createMockInputs();
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
// Should not throw
|
||||
expect(
|
||||
(action as unknown as CoderTaskAction).commentOnIssue(
|
||||
"https://coder.test/tasks/testuser/task-123",
|
||||
"owner",
|
||||
"repo",
|
||||
123,
|
||||
),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
test("updates last comment when multiple Task created comments exist", async () => {
|
||||
octokit.rest.issues.listComments.mockResolvedValue({
|
||||
data: [
|
||||
{ id: 1, body: "Task created: url1" },
|
||||
{ id: 2, body: "Other comment" },
|
||||
{ id: 3, body: "Task created: url2" },
|
||||
{ id: 4, body: "Another comment" },
|
||||
{ id: 5, body: "Task created: url3" },
|
||||
],
|
||||
} as ReturnType<typeof octokit.rest.issues.listComments>);
|
||||
octokit.rest.issues.updateComment.mockResolvedValue(
|
||||
{} as ReturnType<typeof octokit.rest.issues.updateComment>,
|
||||
);
|
||||
|
||||
const inputs = createMockInputs();
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
await (action as unknown as CoderTaskAction).commentOnIssue(
|
||||
"https://coder.test/tasks/testuser/task-123",
|
||||
"owner",
|
||||
"repo",
|
||||
123,
|
||||
);
|
||||
|
||||
// Should update comment 5 (last Task created comment)
|
||||
expect(octokit.rest.issues.updateComment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
comment_id: 5,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("creates new task successfully", async () => {
|
||||
// Setup
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTask.mockResolvedValue(null);
|
||||
coderClient.mockCreateTask.mockResolvedValue(mockTask);
|
||||
|
||||
const inputs = createMockInputs({
|
||||
githubUserID: 12345,
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
// Execute
|
||||
const result = await action.run();
|
||||
|
||||
// Verify
|
||||
expect(coderClient.mockGetCoderUserByGithubID).toHaveBeenCalledWith(12345);
|
||||
expect(coderClient.mockGetTask).toHaveBeenCalledWith(
|
||||
mockUser.username,
|
||||
mockTask.name,
|
||||
);
|
||||
expect(coderClient.mockCreateTask).toHaveBeenCalledWith({
|
||||
username: mockUser.username,
|
||||
name: mockTask.name,
|
||||
template_id: mockTemplate.id,
|
||||
input: "idk",
|
||||
});
|
||||
expect(result.coderUsername).toBe("testuser");
|
||||
expect(result.taskCreated).toBe(false);
|
||||
expect(result.taskUrl).toContain("/tasks/testuser/");
|
||||
});
|
||||
|
||||
test("sends prompt to existing task", async () => {
|
||||
// Setup
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue(
|
||||
mockTemplate,
|
||||
);
|
||||
coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]);
|
||||
coderClient.mockGetTask.mockResolvedValue(mockTask);
|
||||
coderClient.mockSendTaskInput.mockResolvedValue(undefined);
|
||||
|
||||
const inputs = createMockInputs({
|
||||
githubUserID: 12345,
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
// Execute
|
||||
const result = await action.run();
|
||||
|
||||
// Verify
|
||||
expect(coderClient.mockGetTask).toHaveBeenCalledWith(
|
||||
mockUser.username,
|
||||
mockTask.name,
|
||||
);
|
||||
expect(coderClient.mockSendTaskInput).toHaveBeenCalledWith(mockTask.id, {
|
||||
prompt: "test prompt",
|
||||
});
|
||||
expect(coderClient.mockCreateTask).not.toHaveBeenCalled();
|
||||
expect(result.taskCreated).toBe(false);
|
||||
});
|
||||
|
||||
test("errors without issue URL", async () => {
|
||||
// Setup
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTask.mockResolvedValue(null);
|
||||
coderClient.mockCreateTask.mockResolvedValue(mockTask);
|
||||
|
||||
const inputs = createMockInputs({
|
||||
githubUserID: 12345,
|
||||
githubIssueURL: undefined,
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
// Execute
|
||||
expect(action.run()).rejects.toThrowError("Missing issue URL");
|
||||
});
|
||||
|
||||
test("comments on issue", async () => {
|
||||
// Setup
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTask.mockResolvedValue(null);
|
||||
coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue(
|
||||
mockTemplate,
|
||||
);
|
||||
coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]);
|
||||
coderClient.mockCreateTask.mockResolvedValue(mockTask);
|
||||
octokit.rest.issues.listComments.mockResolvedValue({
|
||||
data: [],
|
||||
} as ReturnType<typeof octokit.rest.issues.listComments>);
|
||||
octokit.rest.issues.createComment.mockResolvedValue(
|
||||
{} as ReturnType<typeof octokit.rest.issues.updateComment>,
|
||||
);
|
||||
|
||||
const inputs = createMockInputs({
|
||||
githubUserID: 12345,
|
||||
githubIssueURL: "https://github.com/owner/repo/issues/123",
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
// Execute
|
||||
await action.run();
|
||||
|
||||
// Verify
|
||||
expect(octokit.rest.issues.createComment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
issue_number: 123,
|
||||
body: "Task created: https://coder.test/tasks/testuser/task-123",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("updates existing comment on issue", async () => {
|
||||
// Setup
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTask.mockResolvedValue(null);
|
||||
coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue(
|
||||
mockTemplate,
|
||||
);
|
||||
coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]);
|
||||
coderClient.mockCreateTask.mockResolvedValue(mockTask);
|
||||
octokit.rest.issues.listComments.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 23455,
|
||||
body: "An unrelated comment",
|
||||
},
|
||||
{
|
||||
id: 23456,
|
||||
body: "Task created:",
|
||||
},
|
||||
{
|
||||
id: 23457,
|
||||
body: "Another unrelated comment",
|
||||
},
|
||||
],
|
||||
} as ReturnType<typeof octokit.rest.issues.listComments>);
|
||||
octokit.rest.issues.updateComment.mockResolvedValue(
|
||||
{} as ReturnType<typeof octokit.rest.issues.updateComment>,
|
||||
);
|
||||
|
||||
const inputs = createMockInputs({
|
||||
githubUserID: 12345,
|
||||
githubIssueURL: "https://github.com/owner/repo/issues/123",
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
// Execute
|
||||
await action.run();
|
||||
|
||||
// Verify
|
||||
expect(octokit.rest.issues.updateComment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
comment_id: 23456,
|
||||
body: "Task created: https://coder.test/tasks/testuser/task-123",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("handles error when comment on issue fails", async () => {
|
||||
// Setup
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTask.mockResolvedValue(null);
|
||||
coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue(
|
||||
mockTemplate,
|
||||
);
|
||||
coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]);
|
||||
coderClient.mockCreateTask.mockResolvedValue(mockTask);
|
||||
octokit.rest.issues.listComments.mockResolvedValue({
|
||||
data: [],
|
||||
} as ReturnType<typeof octokit.rest.issues.listComments>);
|
||||
octokit.rest.issues.createComment.mockRejectedValue(
|
||||
new Error("Failed to comment on issue"),
|
||||
);
|
||||
|
||||
const inputs = createMockInputs({
|
||||
githubUserID: 12345,
|
||||
githubIssueURL: "https://github.com/owner/repo/issues/123",
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
await action.run();
|
||||
expect(octokit.rest.issues.createComment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
issue_number: 123,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("run - Error Scenarios", () => {
|
||||
test("throws error when Coder user not found", async () => {
|
||||
coderClient.mockGetCoderUserByGithubID.mockRejectedValue(
|
||||
new Error("No Coder user found with GitHub user ID 12345"),
|
||||
);
|
||||
|
||||
const inputs = createMockInputs({ githubUserID: 12345 });
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
expect(action.run()).rejects.toThrow(
|
||||
"No Coder user found with GitHub user ID 12345",
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error when template not found", async () => {
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTask.mockResolvedValue(null);
|
||||
coderClient.mockGetTemplateByOrganizationAndName.mockRejectedValue(
|
||||
new Error("Template not found"),
|
||||
);
|
||||
coderClient.mockCreateTask.mockRejectedValue(
|
||||
new Error("Template not found: nonexistent"),
|
||||
);
|
||||
|
||||
const inputs = createMockInputs({
|
||||
githubUserID: 12345,
|
||||
coderTemplateName: "nonexistent",
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
expect(action.run()).rejects.toThrow("Template not found");
|
||||
});
|
||||
|
||||
test("throws error when task creation fails", async () => {
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTask.mockResolvedValue(null);
|
||||
coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue(
|
||||
mockTemplate,
|
||||
);
|
||||
coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]);
|
||||
coderClient.mockCreateTask.mockRejectedValue(
|
||||
new Error("Failed to create task"),
|
||||
);
|
||||
|
||||
const inputs = createMockInputs({ githubUserID: 12345 });
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
expect(action.run()).rejects.toThrow("Failed to create task");
|
||||
});
|
||||
|
||||
test("throws error on permission denied", async () => {
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTask.mockResolvedValue(null);
|
||||
coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue(
|
||||
mockTemplate,
|
||||
);
|
||||
coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]);
|
||||
coderClient.mockCreateTask.mockRejectedValue(
|
||||
new Error("Permission denied"),
|
||||
);
|
||||
|
||||
const inputs = createMockInputs({ githubUserID: 12345 });
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
expect(action.run()).rejects.toThrow("Permission denied");
|
||||
});
|
||||
});
|
||||
|
||||
// NOTE: this may or may not work in the real world depending on the permissions of the user
|
||||
test("handles cross-repository issue", async () => {
|
||||
// Setup
|
||||
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
|
||||
coderClient.mockGetTask.mockResolvedValue(null);
|
||||
coderClient.mockGetTemplateByOrganizationAndName.mockResolvedValue(
|
||||
mockTemplate,
|
||||
);
|
||||
coderClient.mockGetTemplateVersionPresets.mockResolvedValue([]);
|
||||
coderClient.mockCreateTask.mockResolvedValue(mockTask);
|
||||
octokit.rest.issues.listComments.mockResolvedValue({
|
||||
data: [],
|
||||
} as ReturnType<typeof octokit.rest.issues.listComments>);
|
||||
octokit.rest.issues.createComment.mockResolvedValue(
|
||||
{} as ReturnType<typeof octokit.rest.issues.createComment>,
|
||||
);
|
||||
|
||||
const inputs = createMockInputs({
|
||||
githubIssueURL:
|
||||
"https://github.com/different-owner/different-repo/issues/456",
|
||||
});
|
||||
const action = new CoderTaskAction(
|
||||
coderClient,
|
||||
octokit as unknown as Octokit,
|
||||
inputs,
|
||||
);
|
||||
|
||||
await action.run();
|
||||
expect(octokit.rest.issues.createComment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
owner: "different-owner",
|
||||
repo: "different-repo",
|
||||
issue_number: 456,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
import * as core from "@actions/core";
|
||||
import {
|
||||
ExperimentalCoderSDKCreateTaskRequest,
|
||||
type CoderClient,
|
||||
} from "./coder-client";
|
||||
import type { ActionInputs, ActionOutputs } from "./schemas";
|
||||
import type { getOctokit } from "@actions/github";
|
||||
|
||||
export type Octokit = ReturnType<typeof getOctokit>;
|
||||
|
||||
export class CoderTaskAction {
|
||||
constructor(
|
||||
private readonly coder: CoderClient,
|
||||
private readonly octokit: Octokit,
|
||||
private readonly inputs: ActionInputs,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parse owner and repo from issue URL
|
||||
*/
|
||||
parseGithubIssueURL(): {
|
||||
githubOrg: string;
|
||||
githubRepo: string;
|
||||
githubIssueNumber: number;
|
||||
} {
|
||||
if (!this.inputs.githubIssueURL) {
|
||||
throw new Error(`Missing issue URL`);
|
||||
}
|
||||
|
||||
// Parse: https://github.com/owner/repo/issues/123
|
||||
const match = this.inputs.githubIssueURL.match(
|
||||
/([^/]+)\/([^/]+)\/issues\/(\d+)/,
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid issue URL: ${this.inputs.githubIssueURL}`);
|
||||
}
|
||||
return {
|
||||
githubOrg: match[1],
|
||||
githubRepo: match[2],
|
||||
githubIssueNumber: parseInt(match[3], 10),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate task URL
|
||||
*/
|
||||
generateTaskUrl(coderUsername: string, taskName: string): string {
|
||||
return `${this.inputs.coderURL}/tasks/${coderUsername}/${taskName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comment on GitHub issue with task link
|
||||
*/
|
||||
async commentOnIssue(
|
||||
taskUrl: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
issueNumber: number,
|
||||
): Promise<void> {
|
||||
const body = `Task created: ${taskUrl}`;
|
||||
|
||||
try {
|
||||
// Try to find existing comment from bot
|
||||
const { data: comments } = await this.octokit.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
// Find the last comment that starts with "Task created:"
|
||||
const existingComment = comments
|
||||
.reverse()
|
||||
.find((comment: { body?: string }) =>
|
||||
comment.body?.startsWith("Task created:"),
|
||||
);
|
||||
|
||||
if (existingComment) {
|
||||
// Update existing comment
|
||||
await this.octokit.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existingComment.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
// Create new comment
|
||||
await this.octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
core.error(`Failed to comment on issue: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main action execution
|
||||
*/
|
||||
async run(): Promise<ActionOutputs> {
|
||||
core.debug(`GitHub user ID: ${this.inputs.githubUserID}`);
|
||||
const coderUser = await this.coder.getCoderUserByGitHubId(
|
||||
this.inputs.githubUserID,
|
||||
);
|
||||
const { githubOrg, githubRepo, githubIssueNumber } =
|
||||
this.parseGithubIssueURL();
|
||||
core.debug(`GitHub owner: ${githubOrg}`);
|
||||
core.debug(`GitHub repo: ${githubRepo}`);
|
||||
core.debug(`GitHub issue number: ${githubIssueNumber}`);
|
||||
core.debug(`Coder username: ${coderUser.username}`);
|
||||
if (!this.inputs.coderTaskNamePrefix || !this.inputs.githubIssueURL) {
|
||||
throw new Error(
|
||||
"either taskName or both taskNamePrefix and issueURL must be provided",
|
||||
);
|
||||
}
|
||||
const taskName = `${this.inputs.coderTaskNamePrefix}-${githubIssueNumber}`;
|
||||
core.debug(`Coder Task name: ${taskName}`);
|
||||
const template = await this.coder.getTemplateByOrganizationAndName(
|
||||
this.inputs.coderOrganization,
|
||||
this.inputs.coderTemplateName,
|
||||
);
|
||||
core.debug(
|
||||
`Coder Template: ${template.name} (id:${template.id}, active_version_id:${template.active_version_id})`,
|
||||
);
|
||||
const templateVersionPresets = await this.coder.getTemplateVersionPresets(
|
||||
template.active_version_id,
|
||||
);
|
||||
let presetID = undefined;
|
||||
// If no preset specified, use default preset
|
||||
if (!this.inputs.coderTemplatePreset) {
|
||||
for (const preset of templateVersionPresets) {
|
||||
if (preset.Name === this.inputs.coderTemplatePreset) {
|
||||
presetID = preset.ID;
|
||||
core.debug(`Coder Template Preset ID: ${presetID}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// User requested a preset that does not exist
|
||||
if (this.inputs.coderTemplatePreset && !presetID) {
|
||||
throw new Error(`Preset ${this.inputs.coderTemplatePreset} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
const existingTask = await this.coder.getTask(coderUser.username, taskName);
|
||||
if (existingTask) {
|
||||
core.debug(`Task already exists: ${existingTask.id}`);
|
||||
core.debug("Sending prompt to existing task...");
|
||||
// Send prompt to existing task
|
||||
await this.coder.sendTaskInput(
|
||||
coderUser.username,
|
||||
taskName,
|
||||
this.inputs.coderTaskPrompt,
|
||||
);
|
||||
core.debug("Prompt sent successfully");
|
||||
return {
|
||||
coderUsername: coderUser.username,
|
||||
taskName: existingTask.name,
|
||||
taskUrl: this.generateTaskUrl(coderUser.username, taskName),
|
||||
taskCreated: false,
|
||||
};
|
||||
}
|
||||
core.debug("Creating Coder task...");
|
||||
|
||||
const req: ExperimentalCoderSDKCreateTaskRequest = {
|
||||
name: taskName,
|
||||
template_version_id: this.inputs.coderTemplateName,
|
||||
template_version_preset_id: presetID,
|
||||
input: this.inputs.coderTaskPrompt,
|
||||
};
|
||||
// Create new task
|
||||
const createdTask = await this.coder.createTask(coderUser.username, req);
|
||||
core.debug("Task created successfully");
|
||||
|
||||
// 5. Generate task URL
|
||||
const taskUrl = this.generateTaskUrl(coderUser.username, createdTask.name);
|
||||
core.debug(`Task URL: ${taskUrl}`);
|
||||
|
||||
// 6. Comment on issue if requested
|
||||
core.debug(
|
||||
`Commenting on issue ${githubOrg}/${githubRepo}#${githubIssueNumber}`,
|
||||
);
|
||||
await this.commentOnIssue(
|
||||
taskUrl,
|
||||
githubOrg,
|
||||
githubRepo,
|
||||
githubIssueNumber,
|
||||
);
|
||||
core.debug(`Comment posted successfully`);
|
||||
return {
|
||||
coderUsername: coderUser.username,
|
||||
taskName: taskName,
|
||||
taskUrl,
|
||||
taskCreated: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
import { describe, expect, test, beforeEach, mock } from "bun:test";
|
||||
import {
|
||||
RealCoderClient,
|
||||
CoderAPIError,
|
||||
ExperimentalCoderSDKCreateTaskRequestSchema,
|
||||
ExperimentalCoderSDKCreateTaskRequest,
|
||||
} from "./coder-client";
|
||||
import {
|
||||
mockUser,
|
||||
mockUserList,
|
||||
mockUserListEmpty,
|
||||
mockUserListDuplicate,
|
||||
mockTemplate,
|
||||
mockTemplateVersionPresets,
|
||||
mockTask,
|
||||
mockTaskList,
|
||||
mockTaskListEmpty,
|
||||
createMockInputs,
|
||||
createMockResponse,
|
||||
mockTemplateVersionPreset,
|
||||
} from "./test-helpers";
|
||||
|
||||
describe("CoderClient", () => {
|
||||
let client: RealCoderClient;
|
||||
let mockFetch: ReturnType<typeof mock>;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockInputs = createMockInputs();
|
||||
client = new RealCoderClient(mockInputs.coderURL, mockInputs.coderToken);
|
||||
mockFetch = mock(() => Promise.resolve(createMockResponse([])));
|
||||
global.fetch = mockFetch as unknown as typeof fetch;
|
||||
});
|
||||
|
||||
describe("getCoderUserByGitHubId", () => {
|
||||
test("returns the user when found", async () => {
|
||||
mockFetch.mockResolvedValue(createMockResponse(mockUserList));
|
||||
const result = await client.getCoderUserByGitHubId(
|
||||
mockUser.github_com_user_id,
|
||||
);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`https://coder.test/api/v2/users?q=github_com_user_id%3A${mockUser.github_com_user_id!.toString()}`,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"Coder-Session-Token": "test-token",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result.id).toBe(mockUser.id);
|
||||
expect(result.username).toBe(mockUser.username);
|
||||
expect(result.github_com_user_id).toBe(mockUser.github_com_user_id);
|
||||
});
|
||||
|
||||
test("throws an error if multiple Coder users are found with the same GitHub ID", async () => {
|
||||
mockFetch.mockResolvedValue(createMockResponse(mockUserListDuplicate));
|
||||
expect(
|
||||
client.getCoderUserByGitHubId(mockUser.github_com_user_id!),
|
||||
).rejects.toThrow(CoderAPIError);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`https://coder.test/api/v2/users?q=github_com_user_id%3A${mockUser.github_com_user_id!.toString()}`,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"Coder-Session-Token": "test-token",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("throws an error if no Coder user is found with the given GitHub ID", async () => {
|
||||
mockFetch.mockResolvedValue(createMockResponse(mockUserListEmpty));
|
||||
expect(
|
||||
client.getCoderUserByGitHubId(mockUser.github_com_user_id!),
|
||||
).rejects.toThrow(CoderAPIError);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`https://coder.test/api/v2/users?q=github_com_user_id%3A${mockUser.github_com_user_id}`,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"Coder-Session-Token": "test-token",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error on 401 unauthorized", async () => {
|
||||
mockFetch.mockResolvedValue(
|
||||
createMockResponse(
|
||||
{ error: "Unauthorized" },
|
||||
{ ok: false, status: 401, statusText: "Unauthorized" },
|
||||
),
|
||||
);
|
||||
expect(
|
||||
client.getCoderUserByGitHubId(mockUser.github_com_user_id!),
|
||||
).rejects.toThrow(CoderAPIError);
|
||||
});
|
||||
|
||||
test("throws error on 500 server error", async () => {
|
||||
mockFetch.mockResolvedValue(
|
||||
createMockResponse(
|
||||
{ error: "Internal Server Error" },
|
||||
{ ok: false, status: 500, statusText: "Internal Server Error" },
|
||||
),
|
||||
);
|
||||
expect(
|
||||
client.getCoderUserByGitHubId(mockUser.github_com_user_id!),
|
||||
).rejects.toThrow(CoderAPIError);
|
||||
});
|
||||
|
||||
test("throws an error when GitHub user ID is 0", async () => {
|
||||
mockFetch.mockResolvedValue(createMockResponse([mockUser]));
|
||||
expect(client.getCoderUserByGitHubId(0)).rejects.toThrow(
|
||||
"GitHub user ID cannot be 0",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTemplateByOrganizationAndName", () => {
|
||||
test("the given template is returned successfully if it exists", async () => {
|
||||
mockFetch.mockResolvedValue(createMockResponse(mockTemplate));
|
||||
const mockInputs = createMockInputs();
|
||||
const result = await client.getTemplateByOrganizationAndName(
|
||||
mockInputs.coderOrganization,
|
||||
mockTemplate.name,
|
||||
);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`https://coder.test/api/v2/organizations/${mockInputs.coderOrganization}/templates/${mockTemplate.name}`,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"Coder-Session-Token": "test-token",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result.id).toBe(mockTemplate.id);
|
||||
expect(result.name).toBe(mockTemplate.name);
|
||||
expect(result.active_version_id).toBe(mockTemplate.active_version_id);
|
||||
});
|
||||
|
||||
test("throws an error when the given template is not found", async () => {
|
||||
mockFetch.mockResolvedValue(
|
||||
createMockResponse(
|
||||
{ error: "Not found" },
|
||||
{ ok: false, status: 404, statusText: "Not Found" },
|
||||
),
|
||||
);
|
||||
const mockInputs = createMockInputs();
|
||||
expect(
|
||||
client.getTemplateByOrganizationAndName(
|
||||
mockInputs.coderOrganization,
|
||||
"nonexistent",
|
||||
),
|
||||
).rejects.toThrow(CoderAPIError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTemplateVersionPresets", () => {
|
||||
test("returns template version presets", async () => {
|
||||
mockFetch.mockResolvedValue(
|
||||
createMockResponse(mockTemplateVersionPresets),
|
||||
);
|
||||
const result = await client.getTemplateVersionPresets(
|
||||
mockTemplate.active_version_id,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toHaveLength(mockTemplateVersionPresets.length);
|
||||
for (let idx = 0; idx < result.length; idx++) {
|
||||
expect(result[idx].ID).toBe(mockTemplateVersionPresets[idx].ID);
|
||||
expect(result[idx].Name).toBe(mockTemplateVersionPresets[idx].Name);
|
||||
}
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`https://coder.test/api/v2/templateversions/${mockTemplate.active_version_id}/presets`,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"Coder-Session-Token": "test-token",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTask", () => {
|
||||
test("returns task when task exists", async () => {
|
||||
mockFetch.mockResolvedValue(createMockResponse(mockTaskList));
|
||||
const result = await client.getTask(mockUser.username, mockTask.name);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.id).toBe(mockTask.id);
|
||||
expect(result?.name).toBe(mockTask.name);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`https://coder.test/api/experimental/tasks?q=owner%3A${mockUser.username}`,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"Coder-Session-Token": "test-token",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("returns null when task doesn't exist (404)", async () => {
|
||||
mockFetch.mockResolvedValue(createMockResponse(mockTaskListEmpty));
|
||||
const result = await client.getTask(mockUser.username, mockTask.name);
|
||||
expect(result).toBeNull();
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`https://coder.test/api/experimental/tasks?q=owner%3A${mockUser.username}`,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"Coder-Session-Token": "test-token",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTask", () => {
|
||||
test("creates task successfully given valid input", async () => {
|
||||
mockFetch.mockResolvedValueOnce(createMockResponse(mockTask));
|
||||
const mockInputs = createMockInputs();
|
||||
const result = await client.createTask(mockUser.username, {
|
||||
name: mockTask.name,
|
||||
template_version_id: mockTemplate.active_version_id,
|
||||
input: mockInputs.coderTaskPrompt,
|
||||
});
|
||||
expect(result.id).toBe(mockTask.id);
|
||||
expect(result.name).toBe(mockTask.name);
|
||||
expect(mockFetch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`https://coder.test/api/experimental/tasks/${mockUser.username}`,
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.objectContaining({
|
||||
"Coder-Session-Token": "test-token",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
name: mockTask.name,
|
||||
template_version_id: mockTemplate.active_version_id,
|
||||
input: mockInputs.coderTaskPrompt,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("creates task successfully with a given preset", async () => {
|
||||
mockFetch.mockResolvedValueOnce(createMockResponse(mockTask));
|
||||
const mockInputs = {
|
||||
...createMockInputs(),
|
||||
template_version_preset_id: mockTemplateVersionPreset.ID,
|
||||
};
|
||||
const result = await client.createTask(mockUser.username, {
|
||||
name: mockTask.name,
|
||||
template_version_id: mockTemplate.active_version_id,
|
||||
template_version_preset_id: mockTemplateVersionPreset.ID,
|
||||
input: mockInputs.coderTaskPrompt,
|
||||
});
|
||||
expect(result.id).toBe(mockTask.id);
|
||||
expect(result.name).toBe(mockTask.name);
|
||||
expect(mockFetch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`https://coder.test/api/experimental/tasks/${mockUser.username}`,
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.objectContaining({
|
||||
"Coder-Session-Token": "test-token",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
name: mockTask.name,
|
||||
template_version_id: mockTemplate.active_version_id,
|
||||
template_version_preset_id: mockTemplateVersionPreset.ID,
|
||||
input: mockInputs.coderTaskPrompt,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendTaskInput", () => {
|
||||
test("sends input successfully", async () => {
|
||||
mockFetch.mockResolvedValue(createMockResponse({}));
|
||||
|
||||
const testInput = "Test input";
|
||||
await client.sendTaskInput(mockUser.username, mockTask.name, testInput);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`https://coder.test/api/v2/users/${mockUser.username}/tasks/${mockTask.name}/send`,
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: expect.stringContaining(testInput),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("request body contains input field", async () => {
|
||||
mockFetch.mockResolvedValue(createMockResponse({}));
|
||||
|
||||
const testInput = "Test input";
|
||||
await client.sendTaskInput(mockUser.username, mockTask.name, testInput);
|
||||
|
||||
const call = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(call[1].body);
|
||||
expect(body.input).toBe(testInput);
|
||||
});
|
||||
|
||||
test("throws error when task not found (404)", async () => {
|
||||
mockFetch.mockResolvedValue(
|
||||
createMockResponse(
|
||||
{ error: "Not Found" },
|
||||
{ ok: false, status: 404, statusText: "Not Found" },
|
||||
),
|
||||
);
|
||||
|
||||
const testInput = "Test input";
|
||||
expect(
|
||||
client.sendTaskInput(mockUser.username, mockTask.name, testInput),
|
||||
).rejects.toThrow(CoderAPIError);
|
||||
});
|
||||
|
||||
test("throws error when task not running (400)", async () => {
|
||||
mockFetch.mockResolvedValue(
|
||||
createMockResponse(
|
||||
{ error: "Bad Request" },
|
||||
{ ok: false, status: 400, statusText: "Bad Request" },
|
||||
),
|
||||
);
|
||||
|
||||
const testInput = "Test input";
|
||||
expect(
|
||||
client.sendTaskInput(mockUser.username, mockTask.name, testInput),
|
||||
).rejects.toThrow(CoderAPIError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export interface CoderClient {
|
||||
getCoderUserByGitHubId(
|
||||
githubUserId: number | undefined,
|
||||
): Promise<CoderSDKUser>;
|
||||
|
||||
getTemplateByOrganizationAndName(
|
||||
organizationName: string,
|
||||
templateName: string,
|
||||
): Promise<CoderSDKTemplate>;
|
||||
|
||||
getTemplateVersionPresets(
|
||||
templateVersionId: string,
|
||||
): Promise<CoderSDKTemplateVersionPreset[]>;
|
||||
|
||||
getTask(
|
||||
owner: string,
|
||||
taskName: string,
|
||||
): Promise<ExperimentalCoderSDKTask | null>;
|
||||
|
||||
createTask(
|
||||
owner: string,
|
||||
params: ExperimentalCoderSDKCreateTaskRequest,
|
||||
): Promise<ExperimentalCoderSDKTask>;
|
||||
|
||||
sendTaskInput(owner: string, taskName: string, input: string): Promise<void>;
|
||||
}
|
||||
|
||||
// CoderClient provides a minimal set of methods for interacting with the Coder API.
|
||||
export class RealCoderClient implements CoderClient {
|
||||
private readonly headers: Record<string, string>;
|
||||
|
||||
constructor(
|
||||
private readonly serverURL: string,
|
||||
apiToken: string,
|
||||
) {
|
||||
this.headers = {
|
||||
"Coder-Session-Token": apiToken,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit,
|
||||
): Promise<T> {
|
||||
const url = `${this.serverURL}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: { ...this.headers, ...options?.headers },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => "");
|
||||
throw new CoderAPIError(
|
||||
`Coder API error: ${response.statusText}`,
|
||||
response.status,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* getCoderUserByGitHubId retrieves an existing Coder user with the given GitHub user ID using Coder's stable API.
|
||||
* Throws an error if more than one user exists with the same GitHub user ID or if a GitHub user ID of 0 is provided.
|
||||
*/
|
||||
async getCoderUserByGitHubId(
|
||||
githubUserId: number | undefined,
|
||||
): Promise<CoderSDKUser> {
|
||||
if (githubUserId === undefined) {
|
||||
throw new CoderAPIError("GitHub user ID cannot be undefined", 400);
|
||||
}
|
||||
if (githubUserId === 0) {
|
||||
throw "GitHub user ID cannot be 0";
|
||||
}
|
||||
const endpoint = `/api/v2/users?q=${encodeURIComponent(`github_com_user_id:${githubUserId}`)}`;
|
||||
const response = await this.request<unknown[]>(endpoint);
|
||||
const userList = CoderSDKGetUsersResponseSchema.parse(response);
|
||||
if (userList.users.length === 0) {
|
||||
throw new CoderAPIError(
|
||||
`No Coder user found with GitHub user ID ${githubUserId}`,
|
||||
404,
|
||||
);
|
||||
}
|
||||
if (userList.users.length > 1) {
|
||||
throw new CoderAPIError(
|
||||
`Multiple Coder users found with GitHub user ID ${githubUserId}`,
|
||||
409,
|
||||
);
|
||||
}
|
||||
return CoderSDKUserSchema.parse(userList.users[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* getTemplateByOrganizationAndName retrieves a template via Coder's stable API.
|
||||
*/
|
||||
async getTemplateByOrganizationAndName(
|
||||
organizationName: string,
|
||||
templateName: string,
|
||||
): Promise<CoderSDKTemplate> {
|
||||
const endpoint = `/api/v2/organizations/${encodeURIComponent(organizationName)}/templates/${encodeURIComponent(templateName)}`;
|
||||
const response =
|
||||
await this.request<typeof CoderSDKTemplateSchema>(endpoint);
|
||||
return CoderSDKTemplateSchema.parse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* getTemplateVersionPresets retrieves the presets for a given template version (UUID).
|
||||
*/
|
||||
async getTemplateVersionPresets(
|
||||
templateVersionId: string,
|
||||
): Promise<CoderSDKTemplateVersionPresetsResponse> {
|
||||
const endpoint = `/api/v2/templateversions/${encodeURIComponent(templateVersionId)}/presets`;
|
||||
const response =
|
||||
await this.request<CoderSDKTemplateVersionPresetsResponse>(endpoint);
|
||||
return CoderSDKTemplateVersionPresetsResponseSchema.parse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* getTask retrieves an existing task via Coder's experimental Tasks API.
|
||||
* Returns null if the task does not exist.
|
||||
*/
|
||||
async getTask(
|
||||
owner: string,
|
||||
taskName: string,
|
||||
): Promise<ExperimentalCoderSDKTask | null> {
|
||||
// TODO: needs taskByOwnerAndName endpoint, fake it for now with the list endpoint.
|
||||
try {
|
||||
const allTasksResponse = await this.request<unknown>(
|
||||
`/api/experimental/tasks?q=${encodeURIComponent(`owner:${owner}`)}`,
|
||||
);
|
||||
const allTasks =
|
||||
ExperimentalCoderSDKTaskListResponseSchema.parse(allTasksResponse);
|
||||
const task = allTasks.tasks.find((t) => t.name === taskName);
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
return task;
|
||||
} catch (error) {
|
||||
if (error instanceof CoderAPIError && error.statusCode === 404) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createTask creates a new task with the given parameters using Coder's experimental Tasks API.
|
||||
*/
|
||||
async createTask(
|
||||
owner: string,
|
||||
params: ExperimentalCoderSDKCreateTaskRequest,
|
||||
): Promise<ExperimentalCoderSDKTask> {
|
||||
const endpoint = `/api/experimental/tasks/${encodeURIComponent(owner)}`;
|
||||
const response = await this.request<unknown>(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
return ExperimentalCoderSDKTaskSchema.parse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* sendTaskInput sends the given input to an existing task via Coder's experimental Tasks API.
|
||||
*/
|
||||
async sendTaskInput(
|
||||
ownerUsername: string,
|
||||
taskName: string,
|
||||
input: string,
|
||||
): Promise<void> {
|
||||
const endpoint = `/api/v2/users/${ownerUsername}/tasks/${taskName}/send`;
|
||||
await this.request<unknown>(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ input }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// CoderSDKUserSchema is the schema for codersdk.User.
|
||||
export const CoderSDKUserSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
username: z.string(),
|
||||
email: z.string().email(),
|
||||
organization_ids: z.array(z.string().uuid()),
|
||||
github_com_user_id: z.number().optional(),
|
||||
});
|
||||
export type CoderSDKUser = z.infer<typeof CoderSDKUserSchema>;
|
||||
|
||||
// CoderSDKUserListSchema is the schema for codersdk.GetUsersResponse.
|
||||
export const CoderSDKGetUsersResponseSchema = z.object({
|
||||
users: z.array(CoderSDKUserSchema),
|
||||
});
|
||||
export type CoderSDKGetUsersResponse = z.infer<
|
||||
typeof CoderSDKGetUsersResponseSchema
|
||||
>;
|
||||
|
||||
// CoderSDKTemplateSchema is the schema for codersdk.Template.
|
||||
export const CoderSDKTemplateSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
organization_id: z.string().uuid(),
|
||||
active_version_id: z.string().uuid(),
|
||||
});
|
||||
export type CoderSDKTemplate = z.infer<typeof CoderSDKTemplateSchema>;
|
||||
|
||||
// CoderSDKTemplateVersionPresetSchema is the schema for codersdk.Preset.
|
||||
export const CoderSDKTemplateVersionPresetSchema = z.object({
|
||||
ID: z.string().uuid(),
|
||||
Name: z.string(),
|
||||
Default: z.boolean(),
|
||||
});
|
||||
export type CoderSDKTemplateVersionPreset = z.infer<
|
||||
typeof CoderSDKTemplateVersionPresetSchema
|
||||
>;
|
||||
|
||||
// CoderSDKTemplateVersionPresetsResponseSchema is the schema for []codersdk.Preset which is returned by the API.
|
||||
export const CoderSDKTemplateVersionPresetsResponseSchema = z.array(
|
||||
CoderSDKTemplateVersionPresetSchema,
|
||||
);
|
||||
export type CoderSDKTemplateVersionPresetsResponse = z.infer<
|
||||
typeof CoderSDKTemplateVersionPresetsResponseSchema
|
||||
>;
|
||||
|
||||
// ExperimentalCoderSDKCreateTaskRequestSchema is the schema for experimental codersdk.CreateTaskRequest.
|
||||
export const ExperimentalCoderSDKCreateTaskRequestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
template_version_id: z.string().min(1),
|
||||
template_version_preset_id: z.string().min(1).optional(),
|
||||
input: z.string().min(1),
|
||||
});
|
||||
export type ExperimentalCoderSDKCreateTaskRequest = z.infer<
|
||||
typeof ExperimentalCoderSDKCreateTaskRequestSchema
|
||||
>;
|
||||
|
||||
// ExperimentalCoderSDKTaskSchema is the schema for experimental codersdk.Task.
|
||||
export const ExperimentalCoderSDKTaskSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
owner_id: z.string().uuid(),
|
||||
template_id: z.string().uuid(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
status: z.string(),
|
||||
});
|
||||
export type ExperimentalCoderSDKTask = z.infer<
|
||||
typeof ExperimentalCoderSDKTaskSchema
|
||||
>;
|
||||
|
||||
// ExperimentalCoderSDKTaskListResponseSchema is the schema for Coder's GET /api/experimental/tasks endpoint.
|
||||
// At the time of writing, this type is not exported by github.com/coder/coder/v2/codersdk.
|
||||
export const ExperimentalCoderSDKTaskListResponseSchema = z.object({
|
||||
tasks: z.array(ExperimentalCoderSDKTaskSchema),
|
||||
});
|
||||
export type ExperimentalCoderSDKTaskListResponse = z.infer<
|
||||
typeof ExperimentalCoderSDKTaskListResponseSchema
|
||||
>;
|
||||
|
||||
// CoderAPIError is a custom error class for Coder API errors.
|
||||
export class CoderAPIError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly statusCode: number,
|
||||
public readonly response?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "CoderAPIError";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as github from "@actions/github";
|
||||
import { CoderTaskAction } from "./action";
|
||||
import { RealCoderClient } from "./coder-client";
|
||||
import { ActionInputsSchema } from "./schemas";
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Parse and validate inputs
|
||||
const inputs = ActionInputsSchema.parse({
|
||||
coderUrl: core.getInput("coder-url", { required: true }),
|
||||
coderToken: core.getInput("coder-token", { required: true }),
|
||||
templateName: core.getInput("template-name", { required: true }),
|
||||
taskPrompt: core.getInput("task-prompt", { required: true }),
|
||||
githubUserId: core.getInput("github-user-id")
|
||||
? Number.parseInt(core.getInput("github-user-id"), 10)
|
||||
: undefined,
|
||||
githubUsername: core.getInput("github-username") || undefined,
|
||||
templatePreset: core.getInput("template-preset") || "Default",
|
||||
taskNamePrefix: core.getInput("task-name-prefix") || "task",
|
||||
taskName: core.getInput("task-name") || undefined,
|
||||
organization: core.getInput("organization") || "coder",
|
||||
issueUrl: core.getInput("issue-url") || undefined,
|
||||
commentOnIssue: core.getBooleanInput("comment-on-issue") !== false,
|
||||
coderWebUrl: core.getInput("coder-web-url") || undefined,
|
||||
githubToken: core.getInput("github-token", { required: true }),
|
||||
});
|
||||
|
||||
core.debug("Inputs validated successfully");
|
||||
core.debug(`Coder URL: ${inputs.coderURL}`);
|
||||
core.debug(`Template: ${inputs.coderTemplateName}`);
|
||||
core.debug(`Organization: ${inputs.coderOrganization}`);
|
||||
|
||||
// Initialize clients
|
||||
const coder = new RealCoderClient(inputs.coderURL, inputs.coderToken);
|
||||
const octokit = github.getOctokit(inputs.githubToken);
|
||||
|
||||
core.debug("Clients initialized");
|
||||
|
||||
// Execute action
|
||||
const action = new CoderTaskAction(coder, octokit, inputs);
|
||||
const outputs = await action.run();
|
||||
|
||||
// Set outputs
|
||||
core.setOutput("coder-username", outputs.coderUsername);
|
||||
core.setOutput("task-name", outputs.taskName);
|
||||
core.setOutput("task-url", outputs.taskUrl);
|
||||
core.setOutput("task-exists", outputs.taskCreated.toString());
|
||||
|
||||
core.debug("Action completed successfully");
|
||||
core.debug(`Outputs: ${JSON.stringify(outputs, null, 2)}`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
core.setFailed(error.message);
|
||||
console.error("Action failed:", error);
|
||||
if (error.stack) {
|
||||
console.error("Stack trace:", error.stack);
|
||||
}
|
||||
} else {
|
||||
core.setFailed("Unknown error occurred");
|
||||
console.error("Unknown error:", error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { ActionInputs, ActionInputsSchema } from "./schemas";
|
||||
|
||||
const actionInputValid: ActionInputs = {
|
||||
coderURL: "https://coder.test",
|
||||
coderToken: "test-token",
|
||||
coderOrganization: "my-org",
|
||||
coderTaskNamePrefix: "gh",
|
||||
coderTaskPrompt: "test prompt",
|
||||
coderTemplateName: "test-template",
|
||||
githubIssueURL: "https://github.com/owner/repo/issues/123",
|
||||
githubToken: "github-token",
|
||||
githubUserID: 12345,
|
||||
coderTemplatePreset: "",
|
||||
};
|
||||
|
||||
describe("ActionInputsSchema", () => {
|
||||
describe("Valid Input Cases", () => {
|
||||
test("accepts minimal required inputs and sets default values correctly", () => {
|
||||
const result = ActionInputsSchema.parse(actionInputValid);
|
||||
expect(result.coderURL).toBe(actionInputValid.coderURL);
|
||||
expect(result.coderToken).toBe(actionInputValid.coderToken);
|
||||
expect(result.coderOrganization).toBe(actionInputValid.coderOrganization);
|
||||
expect(result.coderTaskNamePrefix).toBe(
|
||||
actionInputValid.coderTaskNamePrefix,
|
||||
);
|
||||
expect(result.coderTaskPrompt).toBe(actionInputValid.coderTaskPrompt);
|
||||
expect(result.coderTemplateName).toBe(actionInputValid.coderTemplateName);
|
||||
expect(result.githubIssueURL).toBe(actionInputValid.githubIssueURL);
|
||||
expect(result.githubToken).toBe(actionInputValid.githubToken);
|
||||
expect(result.githubUserID).toBe(actionInputValid.githubUserID);
|
||||
expect(result.coderTemplatePreset).toBeEmpty();
|
||||
});
|
||||
|
||||
test("accepts all optional inputs", () => {
|
||||
const input: ActionInputs = {
|
||||
...actionInputValid,
|
||||
coderTemplatePreset: "custom",
|
||||
};
|
||||
const result = ActionInputsSchema.parse(input);
|
||||
expect(result.coderTemplatePreset).toBe(input.coderTemplatePreset);
|
||||
});
|
||||
|
||||
test("accepts valid URL formats", () => {
|
||||
const validUrls = [
|
||||
"https://coder.test",
|
||||
"https://coder.example.com:8080",
|
||||
"http://12.34.56.78",
|
||||
"https://12.34.56.78:9000",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://[::1]:3000",
|
||||
];
|
||||
|
||||
for (const url of validUrls) {
|
||||
const input: ActionInputs = {
|
||||
...actionInputValid,
|
||||
coderURL: url,
|
||||
};
|
||||
const result = ActionInputsSchema.parse(input);
|
||||
expect(result.coderURL).toBe(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Invalid Input Cases", () => {
|
||||
test("rejects missing required fields", () => {
|
||||
const input = {} as ActionInputs;
|
||||
expect(() => ActionInputsSchema.parse(input)).toThrow();
|
||||
});
|
||||
|
||||
test("rejects invalid URL format for coderUrl", () => {
|
||||
const input: ActionInputs = {
|
||||
...actionInputValid,
|
||||
coderURL: "not-a-url",
|
||||
};
|
||||
expect(() => ActionInputsSchema.parse(input)).toThrow();
|
||||
});
|
||||
|
||||
test("rejects invalid URL format for issueUrl", () => {
|
||||
const input: ActionInputs = {
|
||||
...actionInputValid,
|
||||
githubIssueURL: "not-a-url",
|
||||
};
|
||||
expect(() => ActionInputsSchema.parse(input)).toThrow();
|
||||
});
|
||||
|
||||
test("rejects empty strings for required fields", () => {
|
||||
const input: ActionInputs = {
|
||||
...actionInputValid,
|
||||
coderToken: "",
|
||||
};
|
||||
expect(() => ActionInputsSchema.parse(input)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type ActionInputs = z.infer<typeof ActionInputsSchema>;
|
||||
|
||||
export const ActionInputsSchema = z.object({
|
||||
// Required
|
||||
coderTaskPrompt: z.string().min(1),
|
||||
coderToken: z.string().min(1),
|
||||
coderURL: z.string().url(),
|
||||
coderOrganization: z.string().min(1),
|
||||
coderTaskNamePrefix: z.string().min(1),
|
||||
coderTemplateName: z.string().min(1),
|
||||
githubIssueURL: z.string().url(),
|
||||
githubToken: z.string(),
|
||||
githubUserID: z.number().min(1),
|
||||
// Optional
|
||||
coderTemplatePreset: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ActionOutputsSchema = z.object({
|
||||
coderUsername: z.string(),
|
||||
taskName: z.string(),
|
||||
taskUrl: z.string().url(),
|
||||
taskCreated: z.boolean(),
|
||||
});
|
||||
|
||||
export type ActionOutputs = z.infer<typeof ActionOutputsSchema>;
|
||||
@@ -0,0 +1,207 @@
|
||||
import { mock } from "bun:test";
|
||||
import { CoderClient } from "./coder-client";
|
||||
import type {
|
||||
CoderSDKUser,
|
||||
CoderSDKGetUsersResponse,
|
||||
CoderSDKTemplate,
|
||||
CoderSDKTemplateVersionPreset,
|
||||
ExperimentalCoderSDKTask,
|
||||
ExperimentalCoderSDKTaskListResponse,
|
||||
ExperimentalCoderSDKCreateTaskRequest,
|
||||
} from "./coder-client";
|
||||
import type { ActionInputs } from "./schemas";
|
||||
|
||||
/**
|
||||
* Mock data for tests
|
||||
*/
|
||||
export const mockUser: CoderSDKUser = {
|
||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
organization_ids: ["660e8400-e29b-41d4-a716-446655440000"],
|
||||
github_com_user_id: 12345,
|
||||
};
|
||||
|
||||
export const mockUserList: CoderSDKGetUsersResponse = {
|
||||
users: [mockUser],
|
||||
};
|
||||
|
||||
export const mockUserListEmpty: CoderSDKGetUsersResponse = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
export const mockUserListDuplicate: CoderSDKGetUsersResponse = {
|
||||
users: [
|
||||
mockUser,
|
||||
{
|
||||
...mockUser,
|
||||
id: "660e8400-e29b-41d4-a716-446655440001",
|
||||
username: "testuser2",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mockTemplate: CoderSDKTemplate = {
|
||||
id: "770e8400-e29b-41d4-a716-446655440000",
|
||||
name: "my-template",
|
||||
description: "AI triage template",
|
||||
organization_id: "660e8400-e29b-41d4-a716-446655440000",
|
||||
active_version_id: "880e8400-e29b-41d4-a716-446655440000",
|
||||
};
|
||||
|
||||
export const mockTemplateVersionPreset: CoderSDKTemplateVersionPreset = {
|
||||
ID: "880e8400-e29b-41d4-a716-446655440000",
|
||||
Name: "default-preset",
|
||||
Default: true,
|
||||
};
|
||||
|
||||
export const mockTemplateVersionPreset2: CoderSDKTemplateVersionPreset = {
|
||||
ID: "990e8400-e29b-41d4-a716-446655440000",
|
||||
Name: "another-preset",
|
||||
Default: false,
|
||||
};
|
||||
|
||||
export const mockTemplateVersionPresets = [
|
||||
mockTemplateVersionPreset,
|
||||
mockTemplateVersionPreset2,
|
||||
];
|
||||
|
||||
export const mockTask: ExperimentalCoderSDKTask = {
|
||||
id: "990e8400-e29b-41d4-a716-446655440000",
|
||||
name: "task-123",
|
||||
owner_id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
template_id: "770e8400-e29b-41d4-a716-446655440000",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
status: "running",
|
||||
};
|
||||
|
||||
export const mockTaskList: ExperimentalCoderSDKTaskListResponse = {
|
||||
tasks: [mockTask],
|
||||
};
|
||||
|
||||
export const mockTaskListEmpty: ExperimentalCoderSDKTaskListResponse = {
|
||||
tasks: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Create mock ActionInputs with defaults
|
||||
*/
|
||||
export function createMockInputs(
|
||||
overrides?: Partial<ActionInputs>,
|
||||
): ActionInputs {
|
||||
return {
|
||||
coderTaskPrompt: "Test prompt",
|
||||
coderToken: "test-token",
|
||||
coderURL: "https://coder.test",
|
||||
coderOrganization: "coder",
|
||||
coderTaskNamePrefix: "task",
|
||||
coderTemplateName: "my-template",
|
||||
githubToken: "github-token",
|
||||
githubIssueURL: "https://github.com/test-org/test-repo/issues/12345",
|
||||
githubUserID: 12345,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock CoderClient for testing
|
||||
*/
|
||||
export class MockCoderClient implements CoderClient {
|
||||
private readonly headers: Record<string, string>;
|
||||
public mockGetCoderUserByGithubID = mock();
|
||||
public mockGetTemplateByOrganizationAndName = mock();
|
||||
public mockGetTemplateVersionPresets = mock();
|
||||
public mockGetTask = mock();
|
||||
public mockCreateTask = mock();
|
||||
public mockSendTaskInput = mock();
|
||||
|
||||
constructor() // private readonly serverURL: string,
|
||||
// apiToken: string,
|
||||
{
|
||||
this.headers = {};
|
||||
}
|
||||
|
||||
async getCoderUserByGitHubId(githubUserId: number): Promise<CoderSDKUser> {
|
||||
return this.mockGetCoderUserByGithubID(githubUserId);
|
||||
}
|
||||
|
||||
async getTemplateByOrganizationAndName(
|
||||
organization: string,
|
||||
templateName: string,
|
||||
): Promise<CoderSDKTemplate> {
|
||||
return this.mockGetTemplateByOrganizationAndName(
|
||||
organization,
|
||||
templateName,
|
||||
);
|
||||
}
|
||||
|
||||
async getTemplateVersionPresets(
|
||||
templateVersionId: string,
|
||||
): Promise<CoderSDKTemplateVersionPreset[]> {
|
||||
return this.mockGetTemplateVersionPresets(templateVersionId);
|
||||
}
|
||||
|
||||
async getTask(
|
||||
username: string,
|
||||
taskName: string,
|
||||
): Promise<ExperimentalCoderSDKTask | null> {
|
||||
return this.mockGetTask(username, taskName);
|
||||
}
|
||||
|
||||
async createTask(
|
||||
username: string,
|
||||
params: ExperimentalCoderSDKCreateTaskRequest,
|
||||
): Promise<ExperimentalCoderSDKTask> {
|
||||
return this.mockCreateTask(username, params);
|
||||
}
|
||||
|
||||
async sendTaskInput(
|
||||
username: string,
|
||||
taskName: string,
|
||||
input: string,
|
||||
): Promise<void> {
|
||||
return this.mockSendTaskInput(username, taskName, input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock Octokit for testing
|
||||
*/
|
||||
export function createMockOctokit() {
|
||||
return {
|
||||
rest: {
|
||||
users: {
|
||||
getByUsername: mock(),
|
||||
},
|
||||
issues: {
|
||||
listComments: mock(),
|
||||
createComment: mock(),
|
||||
updateComment: mock(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock fetch for testing
|
||||
*/
|
||||
export function createMockFetch() {
|
||||
return mock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create mock fetch response
|
||||
*/
|
||||
export function createMockResponse(
|
||||
body: unknown,
|
||||
options: { ok?: boolean; status?: number; statusText?: string } = {},
|
||||
) {
|
||||
return {
|
||||
ok: options.ok ?? true,
|
||||
status: options.status ?? 200,
|
||||
statusText: options.statusText ?? "OK",
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
+36
-121
@@ -41,70 +41,25 @@ jobs:
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
# This is only required for testing locally using nektos/act, so leaving commented out.
|
||||
# An alternative is to use a larger or custom image.
|
||||
# - name: Install Github CLI
|
||||
# id: install-gh
|
||||
# run: |
|
||||
# (type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y)) \
|
||||
# && sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||
# && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
# && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
# && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
# && sudo mkdir -p -m 755 /etc/apt/sources.list.d \
|
||||
# && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||
# && sudo apt update \
|
||||
# && sudo apt install gh -y
|
||||
|
||||
- name: Determine Inputs
|
||||
id: determine-inputs
|
||||
if: always()
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_EVENT_ISSUE_HTML_URL: ${{ github.event.issue.html_url }}
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
GITHUB_EVENT_USER_ID: ${{ github.event.sender.id }}
|
||||
GITHUB_EVENT_USER_LOGIN: ${{ github.event.sender.login }}
|
||||
INPUTS_ISSUE_URL: ${{ inputs.issue_url }}
|
||||
INPUTS_TEMPLATE_NAME: ${{ inputs.template_name || 'traiage' }}
|
||||
INPUTS_TEMPLATE_PRESET: ${{ inputs.template_preset || 'Default'}}
|
||||
INPUTS_PREFIX: ${{ inputs.prefix || 'traiage' }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "Using template name: ${INPUTS_TEMPLATE_NAME}"
|
||||
echo "template_name=${INPUTS_TEMPLATE_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "Using template preset: ${INPUTS_TEMPLATE_PRESET}"
|
||||
echo "template_preset=${INPUTS_TEMPLATE_PRESET}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "Using prefix: ${INPUTS_PREFIX}"
|
||||
echo "prefix=${INPUTS_PREFIX}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
# For workflow_dispatch, use the actor who triggered it
|
||||
# For issues events, use the issue author.
|
||||
# Determine issue URL based on event type
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
|
||||
if ! GITHUB_USER_ID=$(gh api "users/${GITHUB_ACTOR}" --jq '.id'); then
|
||||
echo "::error::Failed to get GitHub user ID for actor ${GITHUB_ACTOR}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using workflow_dispatch actor: ${GITHUB_ACTOR} (ID: ${GITHUB_USER_ID})"
|
||||
echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}"
|
||||
echo "github_username=${GITHUB_ACTOR}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "Using issue URL: ${INPUTS_ISSUE_URL}"
|
||||
echo "issue_url=${INPUTS_ISSUE_URL}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
exit 0
|
||||
elif [[ "${GITHUB_EVENT_NAME}" == "issues" ]]; then
|
||||
GITHUB_USER_ID=${GITHUB_EVENT_USER_ID}
|
||||
echo "Using issue author: ${GITHUB_EVENT_USER_LOGIN} (ID: ${GITHUB_USER_ID})"
|
||||
echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}"
|
||||
echo "github_username=${GITHUB_EVENT_USER_LOGIN}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "Using issue URL: ${GITHUB_EVENT_ISSUE_HTML_URL}"
|
||||
echo "issue_url=${GITHUB_EVENT_ISSUE_HTML_URL}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
exit 0
|
||||
else
|
||||
echo "::error::Unsupported event type: ${GITHUB_EVENT_NAME}"
|
||||
exit 1
|
||||
@@ -113,80 +68,36 @@ jobs:
|
||||
- name: Verify push access
|
||||
env:
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_EVENT_USER_LOGIN: ${{ github.event.sender.login }}
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_USERNAME: ${{ steps.determine-inputs.outputs.github_username }}
|
||||
GITHUB_USER_ID: ${{ steps.determine-inputs.outputs.github_user_id }}
|
||||
run: |
|
||||
# Query the actor’s permission on this repo
|
||||
can_push="$(gh api "/repos/${GITHUB_REPOSITORY}/collaborators/${GITHUB_USERNAME}/permission" --jq '.user.permissions.push')"
|
||||
# Determine username based on event type
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
|
||||
USERNAME="${GITHUB_ACTOR}"
|
||||
else
|
||||
USERNAME="${GITHUB_EVENT_USER_LOGIN}"
|
||||
fi
|
||||
|
||||
# Query the user's permission on this repo
|
||||
can_push="$(gh api "/repos/${GITHUB_REPOSITORY}/collaborators/${USERNAME}/permission" --jq '.user.permissions.push')"
|
||||
if [[ "${can_push}" != "true" ]]; then
|
||||
echo "::error title=Access Denied::${GITHUB_USERNAME} does not have push access to ${GITHUB_REPOSITORY}"
|
||||
echo "::error title=Access Denied::${USERNAME} does not have push access to ${GITHUB_REPOSITORY}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Extract context key from issue
|
||||
id: extract-context
|
||||
env:
|
||||
ISSUE_URL: ${{ steps.determine-inputs.outputs.issue_url }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
issue_number="$(gh issue view "${ISSUE_URL}" --json number --jq '.number')"
|
||||
context_key="gh-${issue_number}"
|
||||
echo "context_key=${context_key}" >> "${GITHUB_OUTPUT}"
|
||||
echo "CONTEXT_KEY=${context_key}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Download and install Coder binary
|
||||
shell: bash
|
||||
env:
|
||||
CODER_URL: ${{ secrets.TRAIAGE_CODER_URL }}
|
||||
run: |
|
||||
if [ "${{ runner.arch }}" == "ARM64" ]; then
|
||||
ARCH="arm64"
|
||||
else
|
||||
ARCH="amd64"
|
||||
fi
|
||||
mkdir -p "${HOME}/.local/bin"
|
||||
curl -fsSL --compressed "$CODER_URL/bin/coder-linux-${ARCH}" -o "${HOME}/.local/bin/coder"
|
||||
chmod +x "${HOME}/.local/bin/coder"
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
coder version
|
||||
coder whoami
|
||||
echo "$HOME/.local/bin" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: Get Coder username from GitHub actor
|
||||
id: get-coder-username
|
||||
env:
|
||||
CODER_SESSION_TOKEN: ${{ secrets.TRAIAGE_CODER_SESSION_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_USER_ID: ${{ steps.determine-inputs.outputs.github_user_id }}
|
||||
run: |
|
||||
user_json=$(
|
||||
coder users list --github-user-id="${GITHUB_USER_ID}" --output=json
|
||||
)
|
||||
coder_username=$(jq -r 'first | .username' <<< "$user_json")
|
||||
[[ -z "${coder_username}" || "${coder_username}" == "null" ]] && echo "No Coder user with GitHub user ID ${GITHUB_USER_ID} found" && exit 1
|
||||
echo "coder_username=${coder_username}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
# TODO(Cian): this is a good use-case for 'recipes'
|
||||
- name: Create Coder task
|
||||
id: create-task
|
||||
- name: Fetch issue description
|
||||
id: fetch-issue
|
||||
env:
|
||||
CODER_USERNAME: ${{ steps.get-coder-username.outputs.coder_username }}
|
||||
CONTEXT_KEY: ${{ steps.extract-context.outputs.context_key }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
ISSUE_URL: ${{ steps.determine-inputs.outputs.issue_url }}
|
||||
PREFIX: ${{ steps.determine-inputs.outputs.prefix }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
TEMPLATE_NAME: ${{ steps.determine-inputs.outputs.template_name }}
|
||||
TEMPLATE_PARAMETERS: ${{ secrets.TRAIAGE_TEMPLATE_PARAMETERS }}
|
||||
TEMPLATE_PRESET: ${{ steps.determine-inputs.outputs.template_preset }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Fetch issue description using `gh` CLI
|
||||
#shellcheck disable=SC2016 # The template string should not be subject to shell expansion
|
||||
@@ -194,24 +105,28 @@ jobs:
|
||||
--json 'title,body,comments' \
|
||||
--template '{{printf "%s\n\n%s\n\nComments:\n" .title .body}}{{range $k, $v := .comments}} - {{index $v.author "login"}}: {{printf "%s\n" $v.body}}{{end}}')
|
||||
|
||||
# Write a prompt to PROMPT_FILE
|
||||
PROMPT=$(cat <<EOF
|
||||
# Create prompt for the task
|
||||
{
|
||||
echo "prompt<<EOF"
|
||||
cat <<PROMPT
|
||||
Fix ${ISSUE_URL}
|
||||
|
||||
Analyze the below GitHub issue description, understand the root cause, and make appropriate changes to resolve the issue.
|
||||
---
|
||||
${issue_description}
|
||||
EOF
|
||||
)
|
||||
export PROMPT
|
||||
PROMPT
|
||||
echo "EOF"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
export TASK_NAME="${PREFIX}-${CONTEXT_KEY}-${RUN_ID}"
|
||||
echo "Creating task: $TASK_NAME"
|
||||
./scripts/traiage.sh create
|
||||
if [[ "${ISSUE_URL}" == "https://github.com/${GITHUB_REPOSITORY}"* ]]; then
|
||||
gh issue comment "${ISSUE_URL}" --body "Task created: https://dev.coder.com/tasks/${CODER_USERNAME}/${TASK_NAME}" --create-if-none --edit-last
|
||||
else
|
||||
echo "Skipping comment on other repo."
|
||||
fi
|
||||
echo "TASK_NAME=${CODER_USERNAME}/${TASK_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
echo "TASK_NAME=${CODER_USERNAME}/${TASK_NAME}" >> "${GITHUB_ENV}"
|
||||
- name: Create Coder Task
|
||||
uses: ./.github/actions/coder-task
|
||||
with:
|
||||
coder-url: ${{ secrets.TRAIAGE_CODER_URL }}
|
||||
coder-token: ${{ secrets.TRAIAGE_CODER_SESSION_TOKEN }}
|
||||
template-name: ${{ steps.determine-inputs.outputs.template_name }}
|
||||
template-preset: ${{ steps.determine-inputs.outputs.template_preset }}
|
||||
task-name-prefix: ${{ steps.determine-inputs.outputs.prefix }}
|
||||
task-prompt: ${{ steps.fetch-issue.outputs.prompt }}
|
||||
issue-url: ${{ steps.determine-inputs.outputs.issue_url }}
|
||||
coder-web-url: "https://dev.coder.com"
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
Reference in New Issue
Block a user