diff --git a/dist/index.mjs b/dist/index.mjs
index 68b49c8..73dc468 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -31689,34 +31689,53 @@ module.exports = parseParams
/******/
/************************************************************************/
var __webpack_exports__ = {};
-/* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(6966);
-/* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(4903);
+
+// EXTERNAL MODULE: ./node_modules/.pnpm/@actions+core@1.11.1/node_modules/@actions/core/lib/core.js
+var core = __nccwpck_require__(6966);
+// EXTERNAL MODULE: ./node_modules/.pnpm/@actions+github@6.0.1/node_modules/@actions/github/lib/github.js
+var github = __nccwpck_require__(4903);
+;// CONCATENATED MODULE: ./src/comment.mjs
+const BASE_URL = "https://worktree.io"; // eslint-disable-line default/no-hardcoded-urls
+
+/**
+ * Builds the platform-agnostic "Open workspace" comment body.
+ *
+ * @param {{ owner: string, repo: string, issue: string|number }} params
+ * @returns {string} markdown comment body
+ */
+function buildCommentBody({ owner, repo, issue }) {
+ const url = `${BASE_URL}/open?owner=${owner}&repo=${repo}&issue=${issue}`;
+
+ return [
+ "A workspace is ready for this issue.",
+ "",
+ `
`,
+ "",
+ `Powered by Worktree · Install`,
+ ].join("\n");
+}
+
+
+
+;// CONCATENATED MODULE: ./src/github/index.mjs
-const BASE_URL = "https://worktree.io"; // eslint-disable-line default/no-hardcoded-urls
async function run() {
- const token = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput("token");
- const octokit = _actions_github__WEBPACK_IMPORTED_MODULE_1__.getOctokit(token);
- const { context } = _actions_github__WEBPACK_IMPORTED_MODULE_1__;
+ const token = core.getInput("token");
+ const octokit = github.getOctokit(token);
+ const { context } = github;
if (context.eventName !== "issues" || context.payload.action !== "opened") {
- _actions_core__WEBPACK_IMPORTED_MODULE_0__.info("Skipping: not an issue opened event.");
+ core.info("Skipping: not an issue opened event.");
return;
}
const { owner, repo } = context.repo;
const issue_number = context.payload.issue.number;
- const url = `${BASE_URL}/open?owner=${owner}&repo=${repo}&issue=${issue_number}`;
- const body = [
- "A workspace is ready for this issue.",
- "",
- `
`,
- "",
- `Powered by Worktree · Install`,
- ].join("\n");
+ const body = buildCommentBody({ owner, repo, issue: issue_number });
await octokit.rest.issues.createComment({
owner,
@@ -31725,8 +31744,8 @@ async function run() {
body,
});
- _actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`Posted workspace link for issue #${issue_number}`);
+ core.info(`Posted workspace link for issue #${issue_number}`);
}
-run().catch(_actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed);
+run().catch(core.setFailed);
diff --git a/package.json b/package.json
index 89864c5..5f341cc 100644
--- a/package.json
+++ b/package.json
@@ -8,9 +8,10 @@
"type": "module",
"main": "dist/index.mjs",
"scripts": {
- "build": "ncc build src/index.mjs -o dist --license licenses.txt",
+ "build": "ncc build src/github/index.mjs -o dist --license licenses.txt",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
+ "test": "node --test 'src/**/*.test.mjs'",
"prepare": "husky"
},
"dependencies": {
diff --git a/src/azure/index.mjs b/src/azure/index.mjs
new file mode 100644
index 0000000..8a51b62
--- /dev/null
+++ b/src/azure/index.mjs
@@ -0,0 +1,63 @@
+import { Buffer } from "node:buffer";
+import console from "node:console";
+import process from "node:process";
+import { buildCommentBody } from "../comment.mjs";
+
+class AzureDevOpsCommentError extends Error {}
+
+// ponytail: PAT auth only (Basic auth, empty username). Service-connection auth
+// is a bigger surface (OAuth/token exchange) with no clear consumer yet — add if
+// a workflow actually needs it. See https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/comments/add
+const API_VERSION = "7.1-preview.3";
+
+/**
+ * Posts a comment on an Azure DevOps work item via the Work Item Comments REST API.
+ *
+ * @param {{ organization: string, project: string, workItemId: string|number, token: string, text: string, fetchImpl?: typeof fetch }} params
+ */
+async function postWorkItemComment({ organization, project, workItemId, token, text, fetchImpl }) {
+ const request = fetchImpl || globalThis.fetch;
+ const url = `https://dev.azure.com/${organization}/${encodeURIComponent(project)}/_apis/wit/workItems/${workItemId}/comments?api-version=${API_VERSION}`; // eslint-disable-line default/no-hardcoded-urls
+ const auth = Buffer.from(`:${token}`).toString("base64");
+
+ const response = await request(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Basic ${auth}`,
+ },
+ body: JSON.stringify({ text }),
+ });
+
+ if (!response.ok) {
+ const detail = await response.text().catch(() => "");
+ throw new AzureDevOpsCommentError(`Azure DevOps API error: ${response.status} ${response.statusText} ${detail}`.trim());
+ }
+
+ return response.json();
+}
+
+// ponytail: trigger wiring (service hook vs pipeline step) is an open design
+// question from the issue, left for follow-up. This adapter assumes it is run
+// with the env vars below already populated by whatever triggers it and only
+// guards on their presence.
+async function run() {
+ const organization = process.env.AZURE_DEVOPS_ORG;
+ const project = process.env.SYSTEM_TEAMPROJECT;
+ const repo = process.env.BUILD_REPOSITORY_NAME || project;
+ const workItemId = process.env.WORKITEM_ID;
+ const token = process.env.AZURE_DEVOPS_TOKEN;
+
+ if (!organization || !project || !workItemId || !token) {
+ console.log("Skipping: missing required Azure DevOps environment variables.");
+ return;
+ }
+
+ const body = buildCommentBody({ owner: organization, repo, issue: workItemId });
+
+ await postWorkItemComment({ organization, project, workItemId, token, text: body });
+
+ console.log(`Posted workspace link for work item #${workItemId}`);
+}
+
+export { AzureDevOpsCommentError, postWorkItemComment, run };
diff --git a/src/azure/index.test.mjs b/src/azure/index.test.mjs
new file mode 100644
index 0000000..d6a5f57
--- /dev/null
+++ b/src/azure/index.test.mjs
@@ -0,0 +1,48 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { Buffer } from "node:buffer";
+import { postWorkItemComment } from "./index.mjs";
+
+test("postWorkItemComment posts to the Work Item Comments REST API with Basic auth", async () => {
+ let capturedUrl;
+ let capturedInit;
+
+ const fetchImpl = async (url, init) => {
+ capturedUrl = url;
+ capturedInit = init;
+ return { ok: true, status: 200, json: async () => ({ id: 1 }) };
+ };
+
+ await postWorkItemComment({
+ organization: "my-org",
+ project: "My Project",
+ workItemId: 42,
+ token: "secret-pat",
+ text: "hello",
+ fetchImpl,
+ });
+
+ assert.equal(
+ capturedUrl,
+ "https://dev.azure.com/my-org/My%20Project/_apis/wit/workItems/42/comments?api-version=7.1-preview.3", // eslint-disable-line default/no-hardcoded-urls
+ );
+ assert.equal(capturedInit.method, "POST");
+ assert.equal(capturedInit.headers.Authorization, `Basic ${Buffer.from(":secret-pat").toString("base64")}`);
+ assert.equal(JSON.parse(capturedInit.body).text, "hello");
+});
+
+test("postWorkItemComment throws on a non-ok response", async () => {
+ const fetchImpl = async () => ({ ok: false, status: 401, statusText: "Unauthorized", text: async () => "nope" });
+
+ await assert.rejects(
+ postWorkItemComment({
+ organization: "my-org",
+ project: "proj",
+ workItemId: 1,
+ token: "bad",
+ text: "hello",
+ fetchImpl,
+ }),
+ /Azure DevOps API error: 401/,
+ );
+});
diff --git a/src/comment.mjs b/src/comment.mjs
new file mode 100644
index 0000000..61addad
--- /dev/null
+++ b/src/comment.mjs
@@ -0,0 +1,21 @@
+const BASE_URL = "https://worktree.io"; // eslint-disable-line default/no-hardcoded-urls
+
+/**
+ * Builds the platform-agnostic "Open workspace" comment body.
+ *
+ * @param {{ owner: string, repo: string, issue: string|number }} params
+ * @returns {string} markdown comment body
+ */
+function buildCommentBody({ owner, repo, issue }) {
+ const url = `${BASE_URL}/open?owner=${owner}&repo=${repo}&issue=${issue}`;
+
+ return [
+ "A workspace is ready for this issue.",
+ "",
+ `
`,
+ "",
+ `Powered by Worktree · Install`,
+ ].join("\n");
+}
+
+export { BASE_URL, buildCommentBody };
diff --git a/src/comment.test.mjs b/src/comment.test.mjs
new file mode 100644
index 0000000..e72b1ff
--- /dev/null
+++ b/src/comment.test.mjs
@@ -0,0 +1,11 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { buildCommentBody } from "./comment.mjs";
+
+test("buildCommentBody includes the workspace link with owner/repo/issue", () => {
+ const body = buildCommentBody({ owner: "worktree-io", repo: "comment-action", issue: 1 });
+
+ assert.match(body, /A workspace is ready for this issue\./);
+ assert.match(body, /owner=worktree-io&repo=comment-action&issue=1/);
+ assert.match(body, /Open workspace/);
+});
diff --git a/src/github/index.mjs b/src/github/index.mjs
new file mode 100644
index 0000000..5046e7d
--- /dev/null
+++ b/src/github/index.mjs
@@ -0,0 +1,30 @@
+import core from "@actions/core";
+import github from "@actions/github";
+import { buildCommentBody } from "../comment.mjs";
+
+async function run() {
+ const token = core.getInput("token");
+ const octokit = github.getOctokit(token);
+ const { context } = github;
+
+ if (context.eventName !== "issues" || context.payload.action !== "opened") {
+ core.info("Skipping: not an issue opened event.");
+ return;
+ }
+
+ const { owner, repo } = context.repo;
+ const issue_number = context.payload.issue.number;
+
+ const body = buildCommentBody({ owner, repo, issue: issue_number });
+
+ await octokit.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number,
+ body,
+ });
+
+ core.info(`Posted workspace link for issue #${issue_number}`);
+}
+
+run().catch(core.setFailed);
diff --git a/src/index.mjs b/src/index.mjs
deleted file mode 100644
index ccf5498..0000000
--- a/src/index.mjs
+++ /dev/null
@@ -1,38 +0,0 @@
-import core from "@actions/core";
-import github from "@actions/github";
-
-const BASE_URL = "https://worktree.io"; // eslint-disable-line default/no-hardcoded-urls
-
-async function run() {
- const token = core.getInput("token");
- const octokit = github.getOctokit(token);
- const { context } = github;
-
- if (context.eventName !== "issues" || context.payload.action !== "opened") {
- core.info("Skipping: not an issue opened event.");
- return;
- }
-
- const { owner, repo } = context.repo;
- const issue_number = context.payload.issue.number;
- const url = `${BASE_URL}/open?owner=${owner}&repo=${repo}&issue=${issue_number}`;
-
- const body = [
- "A workspace is ready for this issue.",
- "",
- `
`,
- "",
- `Powered by Worktree · Install`,
- ].join("\n");
-
- await octokit.rest.issues.createComment({
- owner,
- repo,
- issue_number,
- body,
- });
-
- core.info(`Posted workspace link for issue #${issue_number}`);
-}
-
-run().catch(core.setFailed);