Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
"",
`<a href="${url}" target="_blank" rel="noopener noreferrer"><img alt="Open workspace →" src="https://img.shields.io/badge/Open_workspace_%E2%86%92-F05032?style=for-the-badge&logo=git&logoColor=white"></a>`,
"",
`<sub>Powered by <a href="${BASE_URL}" target="_blank" rel="noopener noreferrer">Worktree</a> · <a href="${BASE_URL}#install" target="_blank" rel="noopener noreferrer">Install</a></sub>`,
].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.",
"",
`<a href="${url}" target="_blank" rel="noopener noreferrer"><img alt="Open workspace →" src="https://img.shields.io/badge/Open_workspace_%E2%86%92-F05032?style=for-the-badge&logo=git&logoColor=white"></a>`,
"",
`<sub>Powered by <a href="${BASE_URL}" target="_blank" rel="noopener noreferrer">Worktree</a> · <a href="${BASE_URL}#install" target="_blank" rel="noopener noreferrer">Install</a></sub>`,
].join("\n");
const body = buildCommentBody({ owner, repo, issue: issue_number });

await octokit.rest.issues.createComment({
owner,
Expand All @@ -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);

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
63 changes: 63 additions & 0 deletions src/azure/index.mjs
Original file line number Diff line number Diff line change
@@ -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 };
48 changes: 48 additions & 0 deletions src/azure/index.test.mjs
Original file line number Diff line number Diff line change
@@ -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/,
);
});
21 changes: 21 additions & 0 deletions src/comment.mjs
Original file line number Diff line number Diff line change
@@ -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.",
"",
`<a href="${url}" target="_blank" rel="noopener noreferrer"><img alt="Open workspace →" src="https://img.shields.io/badge/Open_workspace_%E2%86%92-F05032?style=for-the-badge&logo=git&logoColor=white"></a>`,
"",
`<sub>Powered by <a href="${BASE_URL}" target="_blank" rel="noopener noreferrer">Worktree</a> · <a href="${BASE_URL}#install" target="_blank" rel="noopener noreferrer">Install</a></sub>`,
].join("\n");
}

export { BASE_URL, buildCommentBody };
11 changes: 11 additions & 0 deletions src/comment.test.mjs
Original file line number Diff line number Diff line change
@@ -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/);
});
30 changes: 30 additions & 0 deletions src/github/index.mjs
Original file line number Diff line number Diff line change
@@ -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);
38 changes: 0 additions & 38 deletions src/index.mjs

This file was deleted.