diff --git a/.github/scripts/release-label-backfill.mjs b/.github/scripts/release-label-backfill.mjs new file mode 100644 index 000000000..4f13e541e --- /dev/null +++ b/.github/scripts/release-label-backfill.mjs @@ -0,0 +1,71 @@ +import { pathToFileURL } from "node:url"; + +const DEPENDENCIES_LABEL = { + name: "dependencies", + color: "0366d6", + description: "Dependency updates and version bumps", +}; + +async function githubRequest(token, path, { method = "GET", body } = {}) { + const response = await fetch(`https://api.github.com${path}`, { + method, + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "User-Agent": "kagent-release-label-backfill", + "X-GitHub-Api-Version": "2022-11-28", + }, + body: body ? JSON.stringify(body) : undefined, + }); + + if (response.status === 204) { + return null; + } + + const text = await response.text(); + const payload = text ? JSON.parse(text) : null; + + if (!response.ok) { + const message = payload?.message ?? response.statusText; + const error = new Error(`${method} ${path} failed: ${response.status} ${message}`); + error.status = response.status; + throw error; + } + + return payload; +} + +export async function ensureDependenciesLabel({ + token = process.env.GITHUB_TOKEN, + repository = process.env.GITHUB_REPOSITORY, +} = {}) { + if (!token) { + throw new Error("GITHUB_TOKEN is required"); + } + if (!repository) { + throw new Error("GITHUB_REPOSITORY is required"); + } + + try { + await githubRequest( + token, + `/repos/${repository}/labels/${encodeURIComponent(DEPENDENCIES_LABEL.name)}`, + ); + console.log(`label exists: ${DEPENDENCIES_LABEL.name}`); + return; + } catch (error) { + if (error.status !== 404) { + throw error; + } + } + + await githubRequest(token, `/repos/${repository}/labels`, { + method: "POST", + body: DEPENDENCIES_LABEL, + }); + console.log(`created label: ${DEPENDENCIES_LABEL.name}`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await ensureDependenciesLabel(); +} diff --git a/.github/scripts/release-label-backfill.test.mjs b/.github/scripts/release-label-backfill.test.mjs new file mode 100644 index 000000000..deb0d64cd --- /dev/null +++ b/.github/scripts/release-label-backfill.test.mjs @@ -0,0 +1,71 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { ensureDependenciesLabel } from "./release-label-backfill.mjs"; + +test("ensureDependenciesLabel skips creation when the label already exists", async () => { + const requests = []; + const originalFetch = global.fetch; + + global.fetch = async (url, options = {}) => { + requests.push({ url, options }); + return new Response(JSON.stringify({ name: "dependencies" }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }; + + try { + await ensureDependenciesLabel({ + token: "test-token", + repository: "kagent-dev/kagent", + }); + } finally { + global.fetch = originalFetch; + } + + assert.equal(requests.length, 1); + assert.match(requests[0].url, /\/repos\/kagent-dev\/kagent\/labels\/dependencies$/); + assert.equal(requests[0].options.method ?? "GET", "GET"); +}); + +test("ensureDependenciesLabel creates the label when it is missing", async () => { + const requests = []; + const originalFetch = global.fetch; + + global.fetch = async (url, options = {}) => { + requests.push({ url, options }); + + if (requests.length === 1) { + return new Response(JSON.stringify({ message: "Not Found" }), { + status: 404, + headers: { "content-type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ name: "dependencies" }), { + status: 201, + headers: { "content-type": "application/json" }, + }); + }; + + try { + await ensureDependenciesLabel({ + token: "test-token", + repository: "kagent-dev/kagent", + }); + } finally { + global.fetch = originalFetch; + } + + assert.equal(requests.length, 2); + assert.match(requests[0].url, /\/repos\/kagent-dev\/kagent\/labels\/dependencies$/); + assert.equal(requests[0].options.method ?? "GET", "GET"); + assert.match(requests[1].url, /\/repos\/kagent-dev\/kagent\/labels$/); + assert.equal(requests[1].options.method, "POST"); + assert.deepEqual(JSON.parse(requests[1].options.body), { + name: "dependencies", + color: "0366d6", + description: "Dependency updates and version bumps", + }); +}); diff --git a/.github/workflows/release-label-backfill.yml b/.github/workflows/release-label-backfill.yml new file mode 100644 index 000000000..10fd0afec --- /dev/null +++ b/.github/workflows/release-label-backfill.yml @@ -0,0 +1,26 @@ +name: Ensure Dependencies Label + +on: + push: + branches: [main] + paths: + - ".github/workflows/release-label-backfill.yml" + - ".github/release.yml" + - ".github/dependabot.yml" + - ".github/scripts/release-label-backfill.mjs" + workflow_dispatch: + +jobs: + ensure-dependencies-label: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Ensure dependencies label exists + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node .github/scripts/release-label-backfill.mjs