commit ef24c532b475cbd671b0f906b1f798df71182893 Author: ren Date: Sun Apr 12 23:25:43 2026 +0800 feat: 添加 Gitea PR 评审 Action 的初始实现 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2bdf416 --- /dev/null +++ b/.gitignore @@ -0,0 +1,149 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# IDE and editors +.vscode/ +.cursor/ +.trae + +# pnpm +.pnpm-store + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b8ea55 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Gitea PR 评审 Action + +这个 Gitea Action 允许你在 Gitea 的 Pull Request 中添加或更新代码评审(Review)评论。它专为自动化代码审阅工具设计,支持精准的代码行评论。 + +## 功能特性 + +- **零配置启动**:自动从运行环境获取 Gitea 地址、仓库名、PR 编号和提交 ID。 +- **行级评论**:支持在代码文件的特定行添加评审意见。 +- **评审总结**:支持提交整体的评审状态(批准、请求修改或仅评论)。 +- **智能更新**:通过唯一标识符(identifier)更新现有评论,避免评论堆积。 + +## 输入参数 + +| 名称 | 描述 | 必填 | 默认值 | +| --- | --- | --- | --- | +| `gitea-token` | 用于 API 认证的 Gitea token | 是 | | +| `body` | 评论的 Markdown 内容 | 是 | | +| `identifier` | 评论的唯一标识符(用于更新) | 否 | | +| `update-if-exists` | 是否更新具有相同标识符的现有评论 | 否 | `false` | +| `event` | 评审类型 (`COMMENT`, `APPROVE`, `REQUEST_CHANGES`) | 否 | `COMMENT` | +| `commit-id` | 提交的 SHA(默认从 PR 自动获取) | 否 | | +| `path` | 文件相对路径(行级评论必填) | 否 | | +| `new-line` | 新文件中的行号 | 否 | | +| `old-line` | 旧文件中的行号 | 否 | | + +### 高级/覆盖参数(通常自动推断) + +| 名称 | 描述 | 默认值 | +| --- | --- | --- | +| `gitea-base-url` | Gitea 实例的基础 URL | `GITEA_SERVER_URL` | +| `repository` | 仓库路径 (owner/repo) | `GITEA_REPOSITORY` | +| `issue-number` | PR 编号 | 自动从事件 Payload 获取 | + +## 使用方法 + +### 1. 简单的评审总结 + +```yaml +- name: 批准 PR + uses: ren/gitea-comments@v0.1.0 + with: + gitea-token: ${{ secrets.GITEA_TOKEN }} + event: 'APPROVE' + body: "代码审阅通过! ✅" +``` + +### 2. 自动化的代码行评论(更新模式) + +非常适合 Linter 或静态分析工具。 + +```yaml +- name: 提交代码规范建议 + uses: ren/gitea-comments@v0.1.0 + with: + gitea-token: ${{ secrets.GITEA_TOKEN }} + path: "src/main.js" + new-line: 15 + body: "建议在此处使用 `const` 而不是 `let`。" + identifier: lint-error-line-15 + update-if-exists: true +``` + +## 许可证 + +MIT diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..83923f1 --- /dev/null +++ b/action.yml @@ -0,0 +1,47 @@ +name: 'Gitea PR Review' +description: 'Add or update code review comments on a Gitea Pull Request' +author: 'ren' +inputs: + gitea-token: + description: 'The Gitea token to authenticate with the API' + required: true + body: + description: 'The markdown body of the comment' + required: true + identifier: + description: 'A unique identifier for the comment, used for updating existing comments' + required: false + update-if-exists: + description: 'Whether to update an existing comment with the same identifier' + required: false + default: 'false' + event: + description: 'The review event (COMMENT, APPROVE, REQUEST_CHANGES)' + required: false + default: 'COMMENT' + commit-id: + description: 'The SHA of the commit (Automatically inferred from PR if not provided)' + required: false + path: + description: 'The relative path to the file (Required for line-specific comments)' + required: false + new-line: + description: 'The line number in the new file' + required: false + old-line: + description: 'The line number in the old file' + required: false + # Advanced/Override Inputs (Usually inferred from environment) + gitea-base-url: + description: 'The base URL of your Gitea instance (Automatically inferred if not provided)' + required: false + repository: + description: 'The repository (e.g., owner/repo) (Automatically inferred if not provided)' + required: false + issue-number: + description: 'The PR number to review (Automatically inferred if not provided)' + required: false + +runs: + using: 'node20' + main: 'src/index.js' diff --git a/package.json b/package.json new file mode 100644 index 0000000..79275f4 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "gitea-comments", + "version": "0.1.0", + "description": "Gitea comments action", + "main": "index.js", + "scripts": { + }, + "keywords": [ + "gitea", + "comments" + ], + "author": "ren", + "private": true, + "packageManager": "pnpm@10.33.0", + "dependencies": { + "@actions/core": "^1.11.1" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..db3c685 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,266 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@actions/core': + specifier: ^1.11.1 + version: 1.11.1 + axios: + specifier: ^1.8.1 + version: 1.15.0 + devDependencies: + '@vercel/ncc': + specifier: ^0.38.3 + version: 0.38.4 + +packages: + + '@actions/core@1.11.1': + resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==} + + '@actions/exec@1.1.1': + resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==} + + '@actions/http-client@2.2.3': + resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==} + + '@actions/io@1.1.3': + resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==} + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@vercel/ncc@0.38.4': + resolution: {integrity: sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==} + hasBin: true + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + +snapshots: + + '@actions/core@1.11.1': + dependencies: + '@actions/exec': 1.1.1 + '@actions/http-client': 2.2.3 + + '@actions/exec@1.1.1': + dependencies: + '@actions/io': 1.1.3 + + '@actions/http-client@2.2.3': + dependencies: + tunnel: 0.0.6 + undici: 5.29.0 + + '@actions/io@1.1.3': {} + + '@fastify/busboy@2.1.1': {} + + '@vercel/ncc@0.38.4': {} + + asynckit@0.4.0: {} + + axios@1.15.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + proxy-from-env@2.1.0: {} + + tunnel@0.0.6: {} + + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 diff --git a/src/gitea.js b/src/gitea.js new file mode 100644 index 0000000..8f66895 --- /dev/null +++ b/src/gitea.js @@ -0,0 +1,76 @@ +/** + * Gitea API Client for Pull Request Reviews + */ +class GiteaClient { + constructor(baseUrl, token) { + this.baseUrl = baseUrl.replace(/\/+$/, ''); + this.token = token; + this.headers = { + 'Authorization': `token ${this.token}`, + 'Content-Type': 'application/json' + }; + } + + async request(path, options = {}) { + const url = `${this.baseUrl}/api/v1${path}`; + const fetchOptions = { + ...options, + headers: { + ...this.headers, + ...options.headers + } + }; + + const response = await fetch(url, fetchOptions); + + if (!response.ok) { + const errorBody = await response.text().catch(() => 'Unable to read error response'); + throw new Error(`Gitea API Error: ${response.status} ${response.statusText} - ${errorBody}`); + } + + if (response.status === 204) { + return null; + } + + return await response.json(); + } + + /** + * List all review comments on a PR + */ + async listPullRequestComments(owner, repo, index) { + return await this.request(`/repos/${owner}/${repo}/pulls/${index}/comments`); + } + + /** + * Create a review comment on a specific line of a file in a PR + */ + async createPullRequestComment(owner, repo, index, { body, path, new_line, old_line, commit_id }) { + return await this.request(`/repos/${owner}/${repo}/pulls/${index}/comments`, { + method: 'POST', + body: JSON.stringify({ body, path, new_line, old_line, commit_id }) + }); + } + + /** + * Update an existing review comment + */ + async updatePullRequestComment(owner, repo, commentId, body) { + return await this.request(`/repos/${owner}/${repo}/pulls/comments/${commentId}`, { + method: 'PATCH', + body: JSON.stringify({ body }) + }); + } + + /** + * Create a review (summary + optional comments) on a PR + */ + async createReview(owner, repo, index, { body, event, comments = [] }) { + return await this.request(`/repos/${owner}/${repo}/pulls/${index}/reviews`, { + method: 'POST', + body: JSON.stringify({ body, event, comments }) + }); + } +} + +module.exports = GiteaClient; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..02955e5 --- /dev/null +++ b/src/index.js @@ -0,0 +1,104 @@ +const core = require('@actions/core'); +const fs = require('fs'); +const GiteaClient = require('./gitea'); + +async function run() { + try { + const token = core.getInput('gitea-token', { required: true }); + const body = core.getInput('body', { required: true }); + const identifier = core.getInput('identifier'); + const updateIfExists = core.getInput('update-if-exists') === 'true'; + + // Review Specific Inputs + const event = core.getInput('event') || 'COMMENT'; + let commitId = core.getInput('commit-id'); + const path = core.getInput('path'); + const newLine = core.getInput('new-line'); + const oldLine = core.getInput('old-line'); + + // Get parameters from environment variables or inputs + const baseUrl = core.getInput('gitea-base-url') || process.env.GITEA_SERVER_URL || process.env.GITHUB_SERVER_URL; + const repository = core.getInput('repository') || process.env.GITEA_REPOSITORY || process.env.GITHUB_REPOSITORY; + let index = core.getInput('issue-number'); + + if (!baseUrl) { + throw new Error('Gitea base URL not found. Please provide gitea-base-url input or set GITEA_SERVER_URL env.'); + } + if (!repository) { + throw new Error('Repository not found. Please provide repository input or set GITEA_REPOSITORY env.'); + } + + // Load event payload to get PR number and commit ID if not provided + const eventPath = process.env.GITEA_EVENT_PATH || process.env.GITHUB_EVENT_PATH; + if (eventPath && fs.existsSync(eventPath)) { + const eventPayload = JSON.parse(fs.readFileSync(eventPath, 'utf8')); + + if (!index) { + index = eventPayload.pull_request ? eventPayload.pull_request.number : (eventPayload.issue ? eventPayload.issue.number : undefined); + } + + if (!commitId && eventPayload.pull_request) { + commitId = eventPayload.pull_request.head.sha; + } + } + + if (!index) { + throw new Error('Issue or PR number not found. Please provide issue-number input.'); + } + + const [owner, repo] = repository.split('/'); + const client = new GiteaClient(baseUrl, token); + + const finalBody = identifier ? `${body}\n\n` : body; + + // 1. Line-specific Review Comment + if (path && commitId && (newLine || oldLine)) { + let existingComment = null; + + if (identifier && updateIfExists) { + core.info(`Searching for existing line-specific comment with identifier: ${identifier}`); + try { + const comments = await client.listPullRequestComments(owner, repo, index); + const tag = ``; + // Find comment that matches identifier AND path AND line + existingComment = comments.find(c => + c.body.includes(tag) && + c.path === path && + (newLine ? c.new_line === parseInt(newLine) : c.old_line === parseInt(oldLine)) + ); + } catch (error) { + core.warning(`Error listing PR comments: ${error.message}`); + } + } + + if (existingComment) { + core.info(`Updating existing line-specific comment (ID: ${existingComment.id})`); + await client.updatePullRequestComment(owner, repo, existingComment.id, finalBody); + } else { + core.info(`Creating a new line-specific review comment on ${path}`); + await client.createPullRequestComment(owner, repo, index, { + body: finalBody, + path, + commit_id: commitId, + new_line: newLine ? parseInt(newLine) : undefined, + old_line: oldLine ? parseInt(oldLine) : undefined + }); + } + } + // 2. Summary Review + else { + core.info(`Creating a PR review with event: ${event}`); + await client.createReview(owner, repo, index, { + body: finalBody, + event: event + }); + } + + core.info('Action completed successfully.'); + + } catch (error) { + core.setFailed(`Action failed with error: ${error.message}`); + } +} + +run();