diff --git a/BUILD.bazel b/BUILD.bazel index 7bc460c..546cced 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -3,6 +3,12 @@ load("@npm//:defs.bzl", "npm_link_all_packages") npm_link_all_packages(name = "node_modules") +js_library( + name = "config_yml", + srcs = ["config.yml"], + visibility = ["//__tests__:__pkg__"], +) + js_library( name = "jest_config", srcs = [ diff --git a/README.md b/README.md index 7ceb609..8fd860f 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,15 @@ repository in your GitHub organization. ## Dashboard -Temper ships with a real-time operations dashboard powered by **thrum** — a Rust/Axum -backend with HTMX and SSE for live agent monitoring. The dashboard provides: +Temper includes a built-in operations dashboard for monitoring compliance and +activity across your organization. The dashboard provides: -- Task queue with approve/reject/retry actions -- Live agent activity grid with streaming stdout/stderr -- Per-task review pages with syntax-colored diffs and gate reports -- Budget tracking, memory inspection, and agent-to-agent messaging +- Organization-wide compliance score with per-repo breakdown +- Repository health cards — branch protection, signed commits, CI status, merge settings, labels +- Active pull request tracker with check status, labels, and age +- Signal feed for real-time webhook events and configuration drift -Run `thrum` alongside the Probot app, or access the dashboard at `/dashboard` when -deployed with the standalone HTTP handler. +Access the dashboard at `/dashboard` when running with the standalone HTTP handler. ## Quick Start diff --git a/__tests__/BUILD.bazel b/__tests__/BUILD.bazel index 08617b8..059fafb 100644 --- a/__tests__/BUILD.bazel +++ b/__tests__/BUILD.bazel @@ -30,6 +30,7 @@ jest_bin.jest_test( ":test_sources", "//src:lib", "//:jest_config", + "//:config_yml", "//:node_modules/jest", "//:node_modules/@babel/core", "//:node_modules/@babel/preset-env", @@ -52,6 +53,7 @@ jest_bin.jest_test( ":test_sources", "//src:lib", "//:jest_config", + "//:config_yml", "//:node_modules/jest", "//:node_modules/@babel/core", "//:node_modules/@babel/preset-env", diff --git a/__tests__/integration/ai-review.test.js b/__tests__/integration/ai-review.test.js index 815f3c2..fd3c972 100644 --- a/__tests__/integration/ai-review.test.js +++ b/__tests__/integration/ai-review.test.js @@ -30,7 +30,6 @@ import { updateReviewStatus, _reviewTimestamps, _resetReviews, - AI_REVIEW_SIGNATURE } from '../../src/ai-review.js'; import { _setConfigForTesting } from '../../src/config.js'; import { getLogger } from '../../src/logger.js'; @@ -398,6 +397,23 @@ describe('ai-review', () => { expect(result).toContain('`aaaaaaa`'); expect(result).not.toContain('a'.repeat(40)); }); + + it('includes repo and branch metadata when provided', () => { + const result = formatReviewComment('review', 42, 'abc1234', { + baseRepo: 'pulseengine/temper', + baseBranch: 'main', + headRepo: 'contributor/temper', + headBranch: 'fix/bug', + }); + expect(result).toContain('contributor/temper:`fix/bug`'); + expect(result).toContain('pulseengine/temper:`main`'); + expect(result).toContain('→'); + }); + + it('omits branch line when meta is not provided', () => { + const result = formatReviewComment('review', 1); + expect(result).not.toContain('→'); + }); }); // ========================================================================= diff --git a/__tests__/integration/app.test.js b/__tests__/integration/app.test.js index 4fcf75f..5fa7b83 100644 --- a/__tests__/integration/app.test.js +++ b/__tests__/integration/app.test.js @@ -298,7 +298,7 @@ describe('app', () => { it('works without getRouter option', () => { const { app } = setupApp({ skipRouter: true }); - expect(app.on).toHaveBeenCalledTimes(4); + expect(app.on).toHaveBeenCalledTimes(5); expect(app.onError).toHaveBeenCalledTimes(1); }); diff --git a/__tests__/unit/schema.test.js b/__tests__/unit/schema.test.js index 376fe1e..987e38e 100644 --- a/__tests__/unit/schema.test.js +++ b/__tests__/unit/schema.test.js @@ -59,4 +59,33 @@ describe('validateConfig', () => { const result = validateConfig({ ai_review: { enabled: true, temperature: 0.3 } }); expect(result.valid).toBe(true); }); + + it('accepts valid self_update config', () => { + const result = validateConfig({ self_update: { enabled: true, repo: 'temper', branch: 'main' } }); + expect(result.valid).toBe(true); + }); + + it('rejects non-boolean self_update.enabled', () => { + const result = validateConfig({ self_update: { enabled: 'yes' } }); + expect(result.valid).toBe(false); + expect(result.errors[0]).toContain('self_update.enabled'); + }); + + it('rejects empty self_update.repo', () => { + const result = validateConfig({ self_update: { repo: '' } }); + expect(result.valid).toBe(false); + expect(result.errors[0]).toContain('self_update.repo'); + }); + + it('rejects non-string self_update.branch', () => { + const result = validateConfig({ self_update: { branch: 123 } }); + expect(result.valid).toBe(false); + expect(result.errors[0]).toContain('self_update.branch'); + }); + + it('rejects non-object self_update', () => { + const result = validateConfig({ self_update: 'yes' }); + expect(result.valid).toBe(false); + expect(result.errors[0]).toContain('self_update'); + }); }); diff --git a/__tests__/unit/self-update.test.js b/__tests__/unit/self-update.test.js new file mode 100644 index 0000000..ea98cd0 --- /dev/null +++ b/__tests__/unit/self-update.test.js @@ -0,0 +1,50 @@ +import { spawn } from 'node:child_process'; +import { triggerSelfUpdate } from '../../src/self-update.js'; + +jest.mock('node:child_process', () => ({ + spawn: jest.fn(() => ({ unref: jest.fn() })) +})); + +describe('triggerSelfUpdate', () => { + const mockLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('spawns the self-update binary as a detached process', () => { + triggerSelfUpdate(mockLogger); + + expect(spawn).toHaveBeenCalledTimes(1); + const [binary, args, opts] = spawn.mock.calls[0]; + expect(binary).toContain('temper-self-update'); + expect(args).toEqual([]); + expect(opts.detached).toBe(true); + expect(opts.stdio).toBe('ignore'); + }); + + it('passes TEMPER_PID and TEMPER_REPO_DIR as env vars', () => { + triggerSelfUpdate(mockLogger); + + const opts = spawn.mock.calls[0][2]; + expect(opts.env.TEMPER_PID).toBe(String(process.pid)); + expect(opts.env.TEMPER_REPO_DIR).toBeDefined(); + }); + + it('calls unref() on the child process', () => { + const mockUnref = jest.fn(); + spawn.mockReturnValue({ unref: mockUnref }); + + triggerSelfUpdate(mockLogger); + + expect(mockUnref).toHaveBeenCalledTimes(1); + }); + + it('logs that the update is being triggered', () => { + triggerSelfUpdate(mockLogger); + + expect(mockLogger.info).toHaveBeenCalledWith( + expect.stringContaining('self-update') + ); + }); +}); diff --git a/config.yml b/config.yml index fce5b89..d643a13 100644 --- a/config.yml +++ b/config.yml @@ -31,8 +31,11 @@ branch_protection: required_status_checks: strict: true contexts: [] - enforce_admins: false - required_pull_request_reviews: null + enforce_admins: true + required_pull_request_reviews: + required_approving_review_count: 1 + dismiss_stale_reviews: true + require_code_owner_reviews: false required_linear_history: false required_conversation_resolution: false restrictions: null @@ -47,6 +50,11 @@ auto_merge: - thrum merge_method: squash +self_update: + enabled: true + repo: temper + branch: main + issue_labels: - name: "bug" color: "d73a4a" diff --git a/jest.config.cjs b/jest.config.cjs index b618468..cc31ea7 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -5,7 +5,7 @@ module.exports = { '^.+\\.js$': ['babel-jest', { configFile: './babel.config.cjs' }] }, transformIgnorePatterns: [ - '/node_modules/(?!probot|@probot|@octokit|before-after-hook|universal-user-agent|universal-github-app-jwt|octokit-auth-probot)' + '/node_modules/(?!\\.pnpm|\\.aspect_rules_js|probot|@probot|@octokit|before-after-hook|universal-user-agent|universal-github-app-jwt|octokit-auth-probot)' ], testMatch: [ '**/__tests__/unit/**/*.test.js', @@ -15,6 +15,7 @@ module.exports = { ], collectCoverageFrom: [ 'src/**/*.js', + '!src/dashboard.js', 'index.js', '!**/node_modules/**' ], diff --git a/package-lock.json b/package-lock.json index 109b909..5934cdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@octokit/auth-app": "^7.2.0", "@octokit/rest": "^22.0.1", "helmet": "^8.1.0", "js-yaml": "^4.1.1", @@ -3678,71 +3679,359 @@ } }, "node_modules/@octokit/auth-app": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.2.0.tgz", - "integrity": "sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.2.2.tgz", + "integrity": "sha512-p6hJtEyQDCJEPN9ijjhEC/kpFHMHN4Gca9r+8S0S8EJi7NaWftaEmexjxxpT1DFBeJpN4u/5RE22ArnyypupJw==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-app": "^9.0.3", - "@octokit/auth-oauth-user": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", + "@octokit/auth-oauth-app": "^8.1.4", + "@octokit/auth-oauth-user": "^5.1.4", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" } }, + "node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/auth-app/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/@octokit/auth-oauth-app": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.3.tgz", - "integrity": "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==", + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.4.tgz", + "integrity": "sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^8.0.3", - "@octokit/auth-oauth-user": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", + "@octokit/auth-oauth-device": "^7.1.5", + "@octokit/auth-oauth-user": "^5.1.4", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" } }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/@octokit/auth-oauth-device": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.3.tgz", - "integrity": "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.5.tgz", + "integrity": "sha512-lR00+k7+N6xeECj0JuXeULQ2TSBB/zjTAmNF2+vyGPDEFx1dgk1hTDmL13MjbSmzusuAmuJD8Pu39rjp9jH6yw==", "license": "MIT", "dependencies": { - "@octokit/oauth-methods": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", + "@octokit/oauth-methods": "^5.1.5", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" } }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/@octokit/auth-oauth-user": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.2.tgz", - "integrity": "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.6.tgz", + "integrity": "sha512-/R8vgeoulp7rJs+wfJ2LtXEVC7pjQTIqDab7wPKwVG6+2v/lUnCOub6vaHmysQBbb45FknM3tbHW8TOVqYHxCw==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^8.0.3", - "@octokit/oauth-methods": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", + "@octokit/auth-oauth-device": "^7.1.5", + "@octokit/oauth-methods": "^5.1.5", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" } }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -3811,29 +4100,101 @@ } }, "node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", - "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", + "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 18" } }, "node_modules/@octokit/oauth-methods": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.2.tgz", - "integrity": "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.5.tgz", + "integrity": "sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw==", "license": "MIT", "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0" + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" } }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/@octokit/openapi-types": { "version": "27.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", @@ -9482,6 +9843,96 @@ "@octokit/core": ">=7" } }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-app": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.2.0.tgz", + "integrity": "sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^9.0.3", + "@octokit/auth-oauth-user": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-app": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.3.tgz", + "integrity": "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.3", + "@octokit/auth-oauth-user": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-device": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.3.tgz", + "integrity": "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-user": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.2.tgz", + "integrity": "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.3", + "@octokit/oauth-methods": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-authorization-url": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", + "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-methods": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.2.tgz", + "integrity": "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", diff --git a/package.json b/package.json index 8331a62..5119b0e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "homepage": "https://github.com/pulseengine/temper#readme", "dependencies": { + "@octokit/auth-app": "^7.2.0", "@octokit/rest": "^22.0.1", "helmet": "^8.1.0", "js-yaml": "^4.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5134d9..92f202e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@octokit/auth-app': + specifier: ^7.2.0 + version: 7.2.2 '@octokit/rest': specifier: ^22.0.1 version: 22.0.1 @@ -784,18 +787,34 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@octokit/auth-app@7.2.2': + resolution: {integrity: sha512-p6hJtEyQDCJEPN9ijjhEC/kpFHMHN4Gca9r+8S0S8EJi7NaWftaEmexjxxpT1DFBeJpN4u/5RE22ArnyypupJw==} + engines: {node: '>= 18'} + '@octokit/auth-app@8.2.0': resolution: {integrity: sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==} engines: {node: '>= 20'} + '@octokit/auth-oauth-app@8.1.4': + resolution: {integrity: sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ==} + engines: {node: '>= 18'} + '@octokit/auth-oauth-app@9.0.3': resolution: {integrity: sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==} engines: {node: '>= 20'} + '@octokit/auth-oauth-device@7.1.5': + resolution: {integrity: sha512-lR00+k7+N6xeECj0JuXeULQ2TSBB/zjTAmNF2+vyGPDEFx1dgk1hTDmL13MjbSmzusuAmuJD8Pu39rjp9jH6yw==} + engines: {node: '>= 18'} + '@octokit/auth-oauth-device@8.0.3': resolution: {integrity: sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==} engines: {node: '>= 20'} + '@octokit/auth-oauth-user@5.1.6': + resolution: {integrity: sha512-/R8vgeoulp7rJs+wfJ2LtXEVC7pjQTIqDab7wPKwVG6+2v/lUnCOub6vaHmysQBbb45FknM3tbHW8TOVqYHxCw==} + engines: {node: '>= 18'} + '@octokit/auth-oauth-user@6.0.2': resolution: {integrity: sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==} engines: {node: '>= 20'} @@ -812,6 +831,10 @@ packages: resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==} engines: {node: '>= 20'} + '@octokit/endpoint@10.1.4': + resolution: {integrity: sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==} + engines: {node: '>= 18'} + '@octokit/endpoint@11.0.2': resolution: {integrity: sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==} engines: {node: '>= 20'} @@ -820,14 +843,25 @@ packages: resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} engines: {node: '>= 20'} + '@octokit/oauth-authorization-url@7.1.1': + resolution: {integrity: sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==} + engines: {node: '>= 18'} + '@octokit/oauth-authorization-url@8.0.0': resolution: {integrity: sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==} engines: {node: '>= 20'} + '@octokit/oauth-methods@5.1.5': + resolution: {integrity: sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw==} + engines: {node: '>= 18'} + '@octokit/oauth-methods@6.0.2': resolution: {integrity: sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==} engines: {node: '>= 20'} + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} + '@octokit/openapi-types@27.0.0': resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} @@ -868,6 +902,10 @@ packages: peerDependencies: '@octokit/core': ^7.0.0 + '@octokit/request-error@6.1.8': + resolution: {integrity: sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==} + engines: {node: '>= 18'} + '@octokit/request-error@7.1.0': resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} engines: {node: '>= 20'} @@ -876,10 +914,17 @@ packages: resolution: {integrity: sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==} engines: {node: '>= 20'} + '@octokit/request@9.2.4': + resolution: {integrity: sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==} + engines: {node: '>= 18'} + '@octokit/rest@22.0.1': resolution: {integrity: sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==} engines: {node: '>= 20'} + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} @@ -1289,6 +1334,9 @@ packages: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -3242,6 +3290,17 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@octokit/auth-app@7.2.2': + dependencies: + '@octokit/auth-oauth-app': 8.1.4 + '@octokit/auth-oauth-user': 5.1.6 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + toad-cache: 3.7.0 + universal-github-app-jwt: 2.2.2 + universal-user-agent: 7.0.3 + '@octokit/auth-app@8.2.0': dependencies: '@octokit/auth-oauth-app': 9.0.3 @@ -3253,6 +3312,14 @@ snapshots: universal-github-app-jwt: 2.2.2 universal-user-agent: 7.0.3 + '@octokit/auth-oauth-app@8.1.4': + dependencies: + '@octokit/auth-oauth-device': 7.1.5 + '@octokit/auth-oauth-user': 5.1.6 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + '@octokit/auth-oauth-app@9.0.3': dependencies: '@octokit/auth-oauth-device': 8.0.3 @@ -3261,6 +3328,13 @@ snapshots: '@octokit/types': 16.0.0 universal-user-agent: 7.0.3 + '@octokit/auth-oauth-device@7.1.5': + dependencies: + '@octokit/oauth-methods': 5.1.5 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + '@octokit/auth-oauth-device@8.0.3': dependencies: '@octokit/oauth-methods': 6.0.2 @@ -3268,6 +3342,14 @@ snapshots: '@octokit/types': 16.0.0 universal-user-agent: 7.0.3 + '@octokit/auth-oauth-user@5.1.6': + dependencies: + '@octokit/auth-oauth-device': 7.1.5 + '@octokit/oauth-methods': 5.1.5 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + '@octokit/auth-oauth-user@6.0.2': dependencies: '@octokit/auth-oauth-device': 8.0.3 @@ -3293,6 +3375,11 @@ snapshots: before-after-hook: 4.0.0 universal-user-agent: 7.0.3 + '@octokit/endpoint@10.1.4': + dependencies: + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + '@octokit/endpoint@11.0.2': dependencies: '@octokit/types': 16.0.0 @@ -3304,8 +3391,17 @@ snapshots: '@octokit/types': 16.0.0 universal-user-agent: 7.0.3 + '@octokit/oauth-authorization-url@7.1.1': {} + '@octokit/oauth-authorization-url@8.0.0': {} + '@octokit/oauth-methods@5.1.5': + dependencies: + '@octokit/oauth-authorization-url': 7.1.1 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + '@octokit/oauth-methods@6.0.2': dependencies: '@octokit/oauth-authorization-url': 8.0.0 @@ -3313,6 +3409,8 @@ snapshots: '@octokit/request-error': 7.1.0 '@octokit/types': 16.0.0 + '@octokit/openapi-types@25.1.0': {} + '@octokit/openapi-types@27.0.0': {} '@octokit/openapi-webhooks-types@12.1.0': {} @@ -3349,6 +3447,10 @@ snapshots: '@octokit/types': 16.0.0 bottleneck: 2.19.5 + '@octokit/request-error@6.1.8': + dependencies: + '@octokit/types': 14.1.0 + '@octokit/request-error@7.1.0': dependencies: '@octokit/types': 16.0.0 @@ -3361,6 +3463,14 @@ snapshots: fast-content-type-parse: 3.0.0 universal-user-agent: 7.0.3 + '@octokit/request@9.2.4': + dependencies: + '@octokit/endpoint': 10.1.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.3 + '@octokit/rest@22.0.1': dependencies: '@octokit/core': 7.0.6 @@ -3368,6 +3478,10 @@ snapshots: '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.6) '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6) + '@octokit/types@14.1.0': + dependencies: + '@octokit/openapi-types': 25.1.0 + '@octokit/types@16.0.0': dependencies: '@octokit/openapi-types': 27.0.0 @@ -3843,6 +3957,8 @@ snapshots: jest-mock: 30.2.0 jest-util: 30.2.0 + fast-content-type-parse@2.0.1: {} + fast-content-type-parse@3.0.0: {} fast-copy@4.0.2: {} diff --git a/src/BUILD.bazel b/src/BUILD.bazel index 40a9188..b299c79 100644 --- a/src/BUILD.bazel +++ b/src/BUILD.bazel @@ -5,6 +5,7 @@ js_library( srcs = glob(["*.js"]), deps = [ "//:node_modules/probot", + "//:node_modules/@octokit/auth-app", "//:node_modules/@octokit/rest", "//:node_modules/js-yaml", "//:node_modules/helmet", diff --git a/src/ai-review.js b/src/ai-review.js index 39a6ca9..d11a895 100644 --- a/src/ai-review.js +++ b/src/ai-review.js @@ -219,9 +219,20 @@ async function callLocalAI(endpoint, model, systemPrompt, userPrompt, options = const AI_REVIEW_SIGNATURE = 'AI Code Review'; -function formatReviewComment(aiResponse, prNumber, headSha) { +function formatReviewComment(aiResponse, prNumber, headSha, meta) { const sanitized = sanitizeAIOutput(aiResponse); + let header = `## ${AI_REVIEW_SIGNATURE} for PR #${prNumber}\n`; + if (meta) { + const base = meta.baseRepo && meta.baseBranch + ? `${meta.baseRepo}:\`${meta.baseBranch}\`` + : meta.baseBranch ? `\`${meta.baseBranch}\`` : ''; + const head = meta.headRepo && meta.headBranch + ? `${meta.headRepo}:\`${meta.headBranch}\`` + : meta.headBranch ? `\`${meta.headBranch}\`` : ''; + if (base && head) header += `${head} → ${base}\n`; + } + let footer = '*This review was generated by a local AI model. ' + 'It is advisory only and may contain inaccuracies.*'; @@ -231,7 +242,7 @@ function formatReviewComment(aiResponse, prNumber, headSha) { } return ( - `## ${AI_REVIEW_SIGNATURE} for PR #${prNumber}\n\n` + + header + '\n' + sanitized + '\n\n---\n' + footer @@ -346,7 +357,12 @@ async function reviewPullRequest(octokit, owner, repo, prNumber) { _reviewTimestamps.set(rateKey, Date.now()); const headSha = prData.data.head?.sha || ''; - const comment = formatReviewComment(aiResponse, prNumber, headSha); + const comment = formatReviewComment(aiResponse, prNumber, headSha, { + baseRepo: prData.data.base?.repo?.full_name || `${owner}/${repo}`, + baseBranch: prData.data.base?.ref || '', + headRepo: prData.data.head?.repo?.full_name || `${owner}/${repo}`, + headBranch: prData.data.head?.ref || '', + }); // Mark previous bot reviews as outdated await supersedePreviousReviews(octokit, owner, repo, prNumber); diff --git a/src/app.js b/src/app.js index b7c156a..149d7a9 100644 --- a/src/app.js +++ b/src/app.js @@ -16,6 +16,7 @@ import { import { handleSignedCommitMerge, checkPRMergeStrategy } from './merge-strategy.js'; import { reviewPullRequest, updateReviewStatus } from './ai-review.js'; import { isProcessed, markProcessed } from './idempotency.js'; +import { triggerSelfUpdate } from './self-update.js'; import { defaultQueue } from './queue.js'; import { applySecurityMiddleware } from './middleware.js'; @@ -458,8 +459,6 @@ function registerApp(app, { getRouter, addHandler } = {}) { const pr = context.payload.pull_request; const sender = context.payload.sender?.login || ""; - const owner = context.payload.repository.owner.login; - const repo = context.payload.repository.name; const isDependabot = sender === "dependabot[bot]" && autoMerge.on_dependabot; const isBotUser = (autoMerge.on_bot_users || []).some( @@ -513,6 +512,33 @@ function registerApp(app, { getRouter, addHandler } = {}) { if (deliveryId) markProcessed(deliveryId); }); + app.on('push', async (context) => { + if (context.log) setLogger(context.log); + const deliveryId = context.id; + if (deliveryId && isProcessed(deliveryId)) { + getLogger().info({ deliveryId }, 'Skipping duplicate webhook delivery'); + return; + } + + const config = getConfig(); + const selfUpdate = config?.self_update; + if (!selfUpdate?.enabled) return; + + const { repository, ref } = context.payload; + const expectedRef = `refs/heads/${selfUpdate.branch || 'main'}`; + + if (repository.name !== selfUpdate.repo || ref !== expectedRef) return; + + getLogger().info( + { repo: repository.name, ref }, + 'Push to own repo detected — triggering self-update' + ); + + triggerSelfUpdate(getLogger()); + + if (deliveryId) markProcessed(deliveryId); + }); + // Add health check endpoint with queue stats if (getRouter) { const router = getRouter('/'); diff --git a/src/dashboard.js b/src/dashboard.js index 4bda607..1f909a5 100644 --- a/src/dashboard.js +++ b/src/dashboard.js @@ -309,7 +309,6 @@ function renderPRsPartial(data) { // Sort PRs within repo by age (oldest first) prs.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); - const hasFailure = prs.some(p => p.checkStatus === 'fail' || p.checkStatus === 'failure'); const statusBadges = []; const passing = prs.filter(p => p.checkStatus === 'pass' || p.checkStatus === 'success').length; const failing = prs.filter(p => p.checkStatus === 'fail' || p.checkStatus === 'failure').length; diff --git a/src/schema.js b/src/schema.js index fe01a82..de52437 100644 --- a/src/schema.js +++ b/src/schema.js @@ -55,6 +55,23 @@ export function validateConfig(config) { } } + if (config.self_update !== undefined) { + const su = config.self_update; + if (typeof su !== 'object' || su === null) { + errors.push('self_update must be an object'); + } else { + if (su.enabled !== undefined && typeof su.enabled !== 'boolean') { + errors.push('self_update.enabled must be a boolean'); + } + if (su.repo !== undefined && (typeof su.repo !== 'string' || su.repo.length === 0)) { + errors.push('self_update.repo must be a non-empty string'); + } + if (su.branch !== undefined && typeof su.branch !== 'string') { + errors.push('self_update.branch must be a string'); + } + } + } + if (config.ai_review?.enabled) { if (config.ai_review.endpoint !== undefined && typeof config.ai_review.endpoint !== 'string') { errors.push('ai_review.endpoint must be a string'); diff --git a/src/self-update.js b/src/self-update.js new file mode 100644 index 0000000..ed6541d --- /dev/null +++ b/src/self-update.js @@ -0,0 +1,28 @@ +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const BINARY_PATH = join(__dirname, '..', 'tools', 'self-update', 'target', 'release', 'temper-self-update'); + +/** + * Spawns the compiled self-update binary as a detached process, then returns + * immediately so the webhook response can complete before the process restarts. + * + * @param {object} logger - Pino-compatible logger + */ +export function triggerSelfUpdate(logger) { + logger.info('Spawning self-update binary'); + + const child = spawn(BINARY_PATH, [], { + detached: true, + stdio: 'ignore', + env: { + ...process.env, + TEMPER_PID: String(process.pid), + TEMPER_REPO_DIR: join(__dirname, '..') + } + }); + + child.unref(); +} diff --git a/tools/self-update/Cargo.toml b/tools/self-update/Cargo.toml new file mode 100644 index 0000000..cd59628 --- /dev/null +++ b/tools/self-update/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "temper-self-update" +version = "0.1.0" +edition = "2021" + +[profile.release] +strip = true +lto = true diff --git a/tools/self-update/src/main.rs b/tools/self-update/src/main.rs new file mode 100644 index 0000000..2550f95 --- /dev/null +++ b/tools/self-update/src/main.rs @@ -0,0 +1,100 @@ +use std::env; +use std::process::Command; +use std::thread; +use std::time::Duration; + +fn run(cmd: &str, args: &[&str], dir: &str) -> Result { + Command::new(cmd) + .args(args) + .current_dir(dir) + .output() + .map_err(|e| format!("failed to run {cmd}: {e}")) +} + +fn main() { + let repo_dir = env::var("TEMPER_REPO_DIR").unwrap_or_else(|_| { + // Default: two levels up from the binary (tools/self-update/ -> repo root) + let exe = env::current_exe().expect("cannot resolve exe path"); + exe.ancestors() + .nth(3) + .expect("cannot resolve repo root") + .to_string_lossy() + .into_owned() + }); + + let pid = env::var("TEMPER_PID").ok(); + + // Let the webhook response complete + thread::sleep(Duration::from_secs(2)); + + eprintln!("[self-update] pulling latest code in {repo_dir}"); + + // Fetch and reset + if let Err(e) = run("git", &["fetch", "origin", "main"], &repo_dir) { + eprintln!("[self-update] git fetch failed: {e}"); + return; + } + + // Check which files changed before resetting + let diff_output = run("git", &["diff", "HEAD", "origin/main", "--name-only"], &repo_dir); + let diff_text = diff_output + .as_ref() + .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) + .unwrap_or_default(); + let needs_install = diff_text.contains("package-lock.json"); + let updater_changed = diff_text.contains("tools/self-update/"); + + if let Err(e) = run("git", &["reset", "--hard", "origin/main"], &repo_dir) { + eprintln!("[self-update] git reset failed: {e}"); + return; + } + + eprintln!("[self-update] code updated"); + + // Rebuild the updater itself if its source changed. + // On failure the current binary on disk is untouched (cargo only + // replaces the output on a successful build), so next invocation + // still uses the last working version. + if updater_changed { + eprintln!("[self-update] updater source changed, rebuilding"); + let updater_dir = format!("{repo_dir}/tools/self-update"); + match run("cargo", &["build", "--release"], &updater_dir) { + Ok(o) if o.status.success() => { + eprintln!("[self-update] updater rebuilt successfully"); + } + Ok(o) => { + let stderr = String::from_utf8_lossy(&o.stderr); + eprintln!("[self-update] updater build failed (keeping old binary): {stderr}"); + } + Err(e) => { + eprintln!("[self-update] could not run cargo: {e} (keeping old binary)"); + } + } + } + + // Reinstall dependencies if package-lock.json changed + if needs_install { + eprintln!("[self-update] package-lock.json changed, running npm ci"); + if let Err(e) = run("npm", &["ci", "--production"], &repo_dir) { + eprintln!("[self-update] npm ci failed: {e}"); + // Continue anyway — old node_modules may still work + } + } + + // Restart: prefer pm2, fall back to killing the process + let has_pm2 = Command::new("pm2") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + + if has_pm2 { + eprintln!("[self-update] restarting via pm2"); + let _ = run("pm2", &["restart", "temper"], &repo_dir); + } else if let Some(pid_str) = pid { + eprintln!("[self-update] sending SIGTERM to pid {pid_str}"); + let _ = run("kill", &[&pid_str], "/"); + } else { + eprintln!("[self-update] no pm2 and no TEMPER_PID — cannot restart"); + } +}