feat: add support for GHES/Gitea instances with new input parameter and API handling

This commit is contained in:
Wei 2025-04-24 02:14:44 +00:00 committed by GitHub
parent 450b15d522
commit 3727904230
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 489 additions and 116 deletions

View File

@ -2,7 +2,7 @@
[![CI](https://github.com/peter-evans/create-pull-request/workflows/CI/badge.svg)](https://github.com/peter-evans/create-pull-request/actions?query=workflow%3ACI) [![CI](https://github.com/peter-evans/create-pull-request/workflows/CI/badge.svg)](https://github.com/peter-evans/create-pull-request/actions?query=workflow%3ACI)
[![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Create%20Pull%20Request-blue.svg?colorA=24292e&colorB=0366d6&style=flat&longCache=true&logo=github)](https://github.com/marketplace/actions/create-pull-request) [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Create%20Pull%20Request-blue.svg?colorA=24292e&colorB=0366d6&style=flat&longCache=true&logo=github)](https://github.com/marketplace/actions/create-pull-request)
A GitHub action to create a pull request for changes to your repository in the actions workspace. A GitHub action to create a pull request for changes to your repository in the actions workspace. This action also supports GHES and Gitea instances.
Changes to a repository in the Actions workspace persist between steps in a workflow. Changes to a repository in the Actions workspace persist between steps in a workflow.
This action is designed to be used in conjunction with other steps that modify or add files to your repository. This action is designed to be used in conjunction with other steps that modify or add files to your repository.
@ -35,6 +35,25 @@ Create Pull Request action will:
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7
``` ```
### Usage with Gitea or GHES
If you're using this action with Gitea, you need to specify the Gitea hostname using the `github-server-url` parameter:
```yml
- name: Create Pull Request
continue-on-error: true
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
github-server-url: gitea.mediatek.inc
base: master
branch: chore/pre-commit-hooks
title: "chore: Update pre-commit hooks"
body: "Update versions of pre-commit hooks to latest version."
commit-message: "chore: update pre-commit hooks"
delete-branch: true
```
You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v7.x.x` You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v7.x.x`
### Workflow permissions ### Workflow permissions
@ -74,6 +93,7 @@ All inputs are **optional**. If not set, sensible defaults will be used.
| `milestone` | The number of the milestone to associate this pull request with. | | | `milestone` | The number of the milestone to associate this pull request with. | |
| `draft` | Create a [draft pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). Valid values are `true` (only on create), `always-true` (on create and update), and `false`. | `false` | | `draft` | Create a [draft pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). Valid values are `true` (only on create), `always-true` (on create and update), and `false`. | `false` |
| `maintainer-can-modify` | Indicates whether [maintainers can modify](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) the pull request. | `true` | | `maintainer-can-modify` | Indicates whether [maintainers can modify](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) the pull request. | `true` |
| `github-server-url` | A comma-separated list of GHES/Gitea hostnames (e.g., 'gitea.example.com,gitea.company.org'). Required when using this action with Gitea instances. | |
#### token #### token

View File

@ -82,6 +82,10 @@ inputs:
maintainer-can-modify: maintainer-can-modify:
description: 'Indicates whether maintainers can modify the pull request.' description: 'Indicates whether maintainers can modify the pull request.'
default: true default: true
github-server-url:
description: >
A comma-separated list of Gitea hostnames (e.g., 'gitea.example.com,gitea.company.org').
Required when using this action with Gitea instances.
outputs: outputs:
pull-request-number: pull-request-number:
description: 'The pull request number' description: 'The pull request number'

233
dist/index.js vendored
View File

@ -1327,11 +1327,12 @@ class GitHubHelper {
if (token) { if (token) {
options.auth = `${token}`; options.auth = `${token}`;
} }
if (githubServerHostname !== 'github.com') { // Check if this is a Gitea instance
options.baseUrl = `https://${githubServerHostname}/api/v3`; this.isGiteaInstance = (0, octokit_client_1.isGitea)(githubServerHostname);
} // Set the appropriate API base URL for GitHub or Gitea
else { options.baseUrl = (0, octokit_client_1.getApiBaseUrl)(githubServerHostname);
options.baseUrl = 'https://api.github.com'; if (this.isGiteaInstance) {
core.info(`Detected Gitea instance at ${githubServerHostname}. Using API endpoint ${options.baseUrl}`);
} }
options.throttle = octokit_client_1.throttleOptions; options.throttle = octokit_client_1.throttleOptions;
this.octokit = new octokit_client_1.Octokit(options); this.octokit = new octokit_client_1.Octokit(options);
@ -1347,10 +1348,21 @@ class GitHubHelper {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const [headOwner] = headRepository.split('/'); const [headOwner] = headRepository.split('/');
const headBranch = `${headOwner}:${inputs.branch}`; const headBranch = `${headOwner}:${inputs.branch}`;
// For Gitea, the head branch format is different - it's just the branch name
const giteaHeadBranch = this.isGiteaInstance ? inputs.branch : headBranch;
// Try to create the pull request // Try to create the pull request
try { try {
core.info(`Attempting creation of pull request`); core.info(`Attempting creation of pull request`);
const { data: pull } = yield this.octokit.rest.pulls.create(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { title: inputs.title, head: headBranch, head_repo: headRepository, base: inputs.base, body: inputs.body, draft: inputs.draft.value, maintainer_can_modify: inputs.maintainerCanModify })); const createParams = Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { title: inputs.title, head: this.isGiteaInstance ? giteaHeadBranch : headBranch, base: inputs.base, body: inputs.body, maintainer_can_modify: inputs.maintainerCanModify });
// Add draft parameter only for GitHub (Gitea doesn't support draft PRs via the API)
if (!this.isGiteaInstance) {
Object.assign(createParams, { draft: inputs.draft.value });
}
// For Gitea, if using fork, we need to specify the head_repo
if (this.isGiteaInstance && inputs.pushToFork) {
Object.assign(createParams, { head_repo: headRepository });
}
const { data: pull } = yield this.octokit.rest.pulls.create(createParams);
core.info(`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`); core.info(`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`);
return { return {
number: pull.number, number: pull.number,
@ -1376,7 +1388,7 @@ class GitHubHelper {
} }
// Update the pull request that exists for this branch and base // Update the pull request that exists for this branch and base
core.info(`Fetching existing pull request`); core.info(`Fetching existing pull request`);
const { data: pulls } = yield this.octokit.rest.pulls.list(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { state: 'open', head: headBranch, base: inputs.base })); const { data: pulls } = yield this.octokit.rest.pulls.list(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { state: 'open', head: this.isGiteaInstance ? giteaHeadBranch : headBranch, base: inputs.base }));
core.info(`Attempting update of pull request`); core.info(`Attempting update of pull request`);
const { data: pull } = yield this.octokit.rest.pulls.update(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { pull_number: pulls[0].number, title: inputs.title, body: inputs.body })); const { data: pull } = yield this.octokit.rest.pulls.update(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { pull_number: pulls[0].number, title: inputs.title, body: inputs.body }));
core.info(`Updated pull request #${pull.number} (${headBranch} => ${inputs.base})`); core.info(`Updated pull request #${pull.number} (${headBranch} => ${inputs.base})`);
@ -1391,11 +1403,22 @@ class GitHubHelper {
} }
getRepositoryParent(headRepository) { getRepositoryParent(headRepository) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const { data: headRepo } = yield this.octokit.rest.repos.get(Object.assign({}, this.parseRepository(headRepository))); try {
if (!headRepo.parent) { const { data: headRepo } = yield this.octokit.rest.repos.get(Object.assign({}, this.parseRepository(headRepository)));
return null; if (!headRepo.parent) {
return null;
}
return headRepo.parent.full_name;
}
catch (error) {
// Gitea may not have the same parent repository structure
// Fall back to null if this fails
if (this.isGiteaInstance) {
core.warning(`Unable to determine parent repository for ${headRepository}. This is expected for Gitea.`);
return null;
}
throw error;
} }
return headRepo.parent.full_name;
}); });
} }
createOrUpdatePullRequest(inputs, baseRepository, headRepository) { createOrUpdatePullRequest(inputs, baseRepository, headRepository) {
@ -1415,35 +1438,73 @@ class GitHubHelper {
// Apply assignees // Apply assignees
if (inputs.assignees.length > 0) { if (inputs.assignees.length > 0) {
core.info(`Applying assignees '${inputs.assignees}'`); core.info(`Applying assignees '${inputs.assignees}'`);
yield this.octokit.rest.issues.addAssignees(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { issue_number: pull.number, assignees: inputs.assignees })); // Gitea has different assignee handling
} if (this.isGiteaInstance) {
// Request reviewers and team reviewers try {
const requestReviewersParams = {}; for (const assignee of inputs.assignees) {
if (inputs.reviewers.length > 0) { yield this.octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { issue_number: pull.number, assignees: [assignee] }));
requestReviewersParams['reviewers'] = inputs.reviewers; }
core.info(`Requesting reviewers '${inputs.reviewers}'`); }
} catch (error) {
if (inputs.teamReviewers.length > 0) { core.warning(`Error assigning users in Gitea: ${utils.getErrorMessage(error)}`);
const teams = utils.stripOrgPrefixFromTeams(inputs.teamReviewers);
requestReviewersParams['team_reviewers'] = teams;
core.info(`Requesting team reviewers '${teams}'`);
}
if (Object.keys(requestReviewersParams).length > 0) {
try {
yield this.octokit.rest.pulls.requestReviewers(Object.assign(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { pull_number: pull.number }), requestReviewersParams));
}
catch (e) {
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
core.error(`Unable to request reviewers. If requesting team reviewers a 'repo' scoped PAT is required.`);
} }
throw e;
} }
else {
// GitHub standard API
yield this.octokit.rest.issues.addAssignees(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { issue_number: pull.number, assignees: inputs.assignees }));
}
}
// Skip reviewers functionality for Gitea as it might not be compatible
if (!this.isGiteaInstance &&
(inputs.reviewers.length > 0 || inputs.teamReviewers.length > 0)) {
const requestReviewersParams = {};
if (inputs.reviewers.length > 0) {
requestReviewersParams['reviewers'] = inputs.reviewers;
core.info(`Requesting reviewers '${inputs.reviewers}'`);
}
if (inputs.teamReviewers.length > 0) {
const teams = utils.stripOrgPrefixFromTeams(inputs.teamReviewers);
requestReviewersParams['team_reviewers'] = teams;
core.info(`Requesting team reviewers '${teams}'`);
}
if (Object.keys(requestReviewersParams).length > 0) {
try {
yield this.octokit.rest.pulls.requestReviewers(Object.assign(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { pull_number: pull.number }), requestReviewersParams));
}
catch (e) {
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
core.error(`Unable to request reviewers. If requesting team reviewers a 'repo' scoped PAT is required.`);
}
throw e;
}
}
}
else if (this.isGiteaInstance &&
(inputs.reviewers.length > 0 || inputs.teamReviewers.length > 0)) {
core.warning('Reviewer assignment is not supported for Gitea instances');
} }
return pull; return pull;
}); });
} }
pushSignedCommits(git, branchCommits, baseCommit, repoPath, branchRepository, branch) { pushSignedCommits(git, branchCommits, baseCommit, repoPath, branchRepository, branch) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
// For Gitea, fall back to standard Git push if signed commits are not supported
if (this.isGiteaInstance) {
core.warning('Signed commits via API may not be fully supported in Gitea. Falling back to standard Git push.');
yield git.push([
'--force-with-lease',
'origin',
`${branch}:refs/heads/${branch}`
]);
// Return a simplified commit response
return {
sha: ((_a = branchCommits[branchCommits.length - 1]) === null || _a === void 0 ? void 0 : _a.sha) || baseCommit.sha,
tree: ((_b = branchCommits[branchCommits.length - 1]) === null || _b === void 0 ? void 0 : _b.tree) || baseCommit.tree,
verified: false
};
}
// Original GitHub implementation
let headCommit = { let headCommit = {
sha: baseCommit.sha, sha: baseCommit.sha,
tree: baseCommit.tree, tree: baseCommit.tree,
@ -1511,43 +1572,89 @@ class GitHubHelper {
} }
const { data: remoteCommit } = yield this.octokit.rest.git.createCommit(Object.assign(Object.assign({}, repository), { parents: [parentCommit.sha], tree: treeSha, message: `${commit.subject}\n\n${commit.body}` })); const { data: remoteCommit } = yield this.octokit.rest.git.createCommit(Object.assign(Object.assign({}, repository), { parents: [parentCommit.sha], tree: treeSha, message: `${commit.subject}\n\n${commit.body}` }));
core.info(`Created commit ${remoteCommit.sha} for local commit ${commit.sha}`); core.info(`Created commit ${remoteCommit.sha} for local commit ${commit.sha}`);
core.info(`Commit verified: ${remoteCommit.verification.verified}; reason: ${remoteCommit.verification.reason}`); // Gitea might not have the same verification structure
let verified = false;
if (remoteCommit.verification &&
typeof remoteCommit.verification.verified !== 'undefined') {
verified = remoteCommit.verification.verified;
core.info(`Commit verified: ${verified}; reason: ${remoteCommit.verification.reason || 'unknown'}`);
}
else {
core.info('Commit verification information not available');
}
return { return {
sha: remoteCommit.sha, sha: remoteCommit.sha,
tree: remoteCommit.tree.sha, tree: remoteCommit.tree.sha,
verified: remoteCommit.verification.verified verified: verified
}; };
}); });
} }
getCommit(sha, branchRepository) { getCommit(sha, branchRepository) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const repository = this.parseRepository(branchRepository); const repository = this.parseRepository(branchRepository);
const { data: remoteCommit } = yield this.octokit.rest.git.getCommit(Object.assign(Object.assign({}, repository), { commit_sha: sha })); try {
return { const { data: remoteCommit } = yield this.octokit.rest.git.getCommit(Object.assign(Object.assign({}, repository), { commit_sha: sha }));
sha: remoteCommit.sha, // Handle different verification structure between GitHub and Gitea
tree: remoteCommit.tree.sha, let verified = false;
verified: remoteCommit.verification.verified if (remoteCommit.verification &&
}; typeof remoteCommit.verification.verified !== 'undefined') {
verified = remoteCommit.verification.verified;
}
return {
sha: remoteCommit.sha,
tree: remoteCommit.tree.sha,
verified: verified
};
}
catch (error) {
if (this.isGiteaInstance) {
core.warning(`Unable to get commit details from Gitea. This might be expected: ${utils.getErrorMessage(error)}`);
// Return a placeholder response
return {
sha: sha,
tree: '', // We don't know the tree SHA
verified: false
};
}
throw error;
}
}); });
} }
createOrUpdateRef(branchRepository, branch, newHead) { createOrUpdateRef(branchRepository, branch, newHead) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const repository = this.parseRepository(branchRepository); const repository = this.parseRepository(branchRepository);
const branchExists = yield this.octokit.rest.repos // Check if branch exists
.getBranch(Object.assign(Object.assign({}, repository), { branch: branch })) let branchExists = false;
.then(() => true, () => false); try {
yield this.octokit.rest.repos.getBranch(Object.assign(Object.assign({}, repository), { branch: branch }));
branchExists = true;
}
catch (_a) {
branchExists = false;
}
if (branchExists) { if (branchExists) {
core.info(`Branch ${branch} exists; Updating ref`); core.info(`Branch ${branch} exists; Updating ref`);
yield this.octokit.rest.git.updateRef(Object.assign(Object.assign({}, repository), { sha: newHead, ref: `heads/${branch}`, force: true })); yield this.octokit.rest.git.updateRef(Object.assign(Object.assign({}, repository), { sha: newHead, ref: `heads/${branch}`, force: true }));
} }
else { else {
core.info(`Branch ${branch} does not exist; Creating ref`); core.info(`Branch ${branch} does not exist; Creating ref`);
yield this.octokit.rest.git.createRef(Object.assign(Object.assign({}, repository), { sha: newHead, ref: `refs/heads/${branch}` })); try {
yield this.octokit.rest.git.createRef(Object.assign(Object.assign({}, repository), { sha: newHead, ref: `refs/heads/${branch}` }));
}
catch (error) {
core.error(`Failed to create branch: ${utils.getErrorMessage(error)}`);
throw error;
}
} }
}); });
} }
convertToDraft(id) { convertToDraft(id) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
// Skip for Gitea since GraphQL API likely isn't compatible
if (this.isGiteaInstance) {
core.warning('Draft pull requests are not supported in Gitea via the GraphQL API');
return;
}
core.info(`Converting pull request to draft`); core.info(`Converting pull request to draft`);
yield this.octokit.graphql({ yield this.octokit.graphql({
query: `mutation($pullRequestId: ID!) { query: `mutation($pullRequestId: ID!) {
@ -1627,9 +1734,26 @@ function getDraftInput() {
return { value: core.getBooleanInput('draft'), always: false }; return { value: core.getBooleanInput('draft'), always: false };
} }
} }
// Set Gitea instances from environment variable or input
function configureGiteaInstances() {
// First check if there's already an environment variable
if (!process.env.GITEA_INSTANCES) {
// If not, check if it was provided as input
const giteaInstancesInput = core.getInput('github-server-url');
if (giteaInstancesInput) {
core.info(`Setting GITEA_INSTANCES environment variable to: ${giteaInstancesInput}`);
process.env.GITEA_INSTANCES = giteaInstancesInput;
}
}
if (process.env.GITEA_INSTANCES) {
core.info(`Configured Gitea instances: ${process.env.GITEA_INSTANCES}`);
}
}
function run() { function run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
// Configure Gitea instances before anything else
configureGiteaInstances();
const inputs = { const inputs = {
token: core.getInput('token'), token: core.getInput('token'),
branchToken: core.getInput('branch-token'), branchToken: core.getInput('branch-token'),
@ -1727,6 +1851,8 @@ var __importStar = (this && this.__importStar) || (function () {
})(); })();
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.throttleOptions = exports.Octokit = void 0; exports.throttleOptions = exports.Octokit = void 0;
exports.isGitea = isGitea;
exports.getApiBaseUrl = getApiBaseUrl;
const core = __importStar(__nccwpck_require__(7484)); const core = __importStar(__nccwpck_require__(7484));
const core_1 = __nccwpck_require__(767); const core_1 = __nccwpck_require__(767);
const plugin_paginate_rest_1 = __nccwpck_require__(3779); const plugin_paginate_rest_1 = __nccwpck_require__(3779);
@ -1754,6 +1880,25 @@ function autoProxyAgent(octokit) {
options.request.fetch = proxy_1.fetch; options.request.fetch = proxy_1.fetch;
}); });
} }
// Determine if a hostname is a Gitea instance
function isGitea(hostname) {
return process.env.GITEA_INSTANCES
? process.env.GITEA_INSTANCES.split(',').includes(hostname)
: false;
}
// Get the API base URL for a given hostname
function getApiBaseUrl(hostname) {
// For GitHub, we'll use their standard API endpoint
if (hostname === 'github.com') {
return 'https://api.github.com';
}
// For Gitea, we need to modify the API path
if (isGitea(hostname)) {
return `https://${hostname}/api/v1`;
}
// For GitHub Enterprise or other GitHub-compatible APIs
return `https://${hostname}/api/v3`;
}
/***/ }), /***/ }),

View File

@ -1,7 +1,13 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Inputs} from './create-pull-request' import {Inputs} from './create-pull-request'
import {Commit, GitCommandManager} from './git-command-manager' import {Commit, GitCommandManager} from './git-command-manager'
import {Octokit, OctokitOptions, throttleOptions} from './octokit-client' import {
Octokit,
OctokitOptions,
throttleOptions,
isGitea,
getApiBaseUrl
} from './octokit-client'
import pLimit from 'p-limit' import pLimit from 'p-limit'
import * as utils from './utils' import * as utils from './utils'
@ -9,7 +15,6 @@ const ERROR_PR_ALREADY_EXISTS = 'A pull request already exists for'
const ERROR_PR_REVIEW_TOKEN_SCOPE = const ERROR_PR_REVIEW_TOKEN_SCOPE =
'Validation Failed: "Could not resolve to a node with the global id of' 'Validation Failed: "Could not resolve to a node with the global id of'
const ERROR_PR_FORK_COLLAB = `Fork collab can't be granted by someone without permission` const ERROR_PR_FORK_COLLAB = `Fork collab can't be granted by someone without permission`
const blobCreationLimit = pLimit(8) const blobCreationLimit = pLimit(8)
interface Repository { interface Repository {
@ -40,17 +45,26 @@ type TreeObject = {
export class GitHubHelper { export class GitHubHelper {
private octokit: InstanceType<typeof Octokit> private octokit: InstanceType<typeof Octokit>
private isGiteaInstance: boolean
constructor(githubServerHostname: string, token: string) { constructor(githubServerHostname: string, token: string) {
const options: OctokitOptions = {} const options: OctokitOptions = {}
if (token) { if (token) {
options.auth = `${token}` options.auth = `${token}`
} }
if (githubServerHostname !== 'github.com') {
options.baseUrl = `https://${githubServerHostname}/api/v3` // Check if this is a Gitea instance
} else { this.isGiteaInstance = isGitea(githubServerHostname)
options.baseUrl = 'https://api.github.com'
// Set the appropriate API base URL for GitHub or Gitea
options.baseUrl = getApiBaseUrl(githubServerHostname)
if (this.isGiteaInstance) {
core.info(
`Detected Gitea instance at ${githubServerHostname}. Using API endpoint ${options.baseUrl}`
)
} }
options.throttle = throttleOptions options.throttle = throttleOptions
this.octokit = new Octokit(options) this.octokit = new Octokit(options)
} }
@ -71,19 +85,33 @@ export class GitHubHelper {
const [headOwner] = headRepository.split('/') const [headOwner] = headRepository.split('/')
const headBranch = `${headOwner}:${inputs.branch}` const headBranch = `${headOwner}:${inputs.branch}`
// For Gitea, the head branch format is different - it's just the branch name
const giteaHeadBranch = this.isGiteaInstance ? inputs.branch : headBranch
// Try to create the pull request // Try to create the pull request
try { try {
core.info(`Attempting creation of pull request`) core.info(`Attempting creation of pull request`)
const {data: pull} = await this.octokit.rest.pulls.create({ const createParams = {
...this.parseRepository(baseRepository), ...this.parseRepository(baseRepository),
title: inputs.title, title: inputs.title,
head: headBranch, head: this.isGiteaInstance ? giteaHeadBranch : headBranch,
head_repo: headRepository,
base: inputs.base, base: inputs.base,
body: inputs.body, body: inputs.body,
draft: inputs.draft.value,
maintainer_can_modify: inputs.maintainerCanModify maintainer_can_modify: inputs.maintainerCanModify
}) }
// Add draft parameter only for GitHub (Gitea doesn't support draft PRs via the API)
if (!this.isGiteaInstance) {
Object.assign(createParams, {draft: inputs.draft.value})
}
// For Gitea, if using fork, we need to specify the head_repo
if (this.isGiteaInstance && inputs.pushToFork) {
Object.assign(createParams, {head_repo: headRepository})
}
const {data: pull} = await this.octokit.rest.pulls.create(createParams)
core.info( core.info(
`Created pull request #${pull.number} (${headBranch} => ${inputs.base})` `Created pull request #${pull.number} (${headBranch} => ${inputs.base})`
) )
@ -116,9 +144,10 @@ export class GitHubHelper {
const {data: pulls} = await this.octokit.rest.pulls.list({ const {data: pulls} = await this.octokit.rest.pulls.list({
...this.parseRepository(baseRepository), ...this.parseRepository(baseRepository),
state: 'open', state: 'open',
head: headBranch, head: this.isGiteaInstance ? giteaHeadBranch : headBranch,
base: inputs.base base: inputs.base
}) })
core.info(`Attempting update of pull request`) core.info(`Attempting update of pull request`)
const {data: pull} = await this.octokit.rest.pulls.update({ const {data: pull} = await this.octokit.rest.pulls.update({
...this.parseRepository(baseRepository), ...this.parseRepository(baseRepository),
@ -126,9 +155,11 @@ export class GitHubHelper {
title: inputs.title, title: inputs.title,
body: inputs.body body: inputs.body
}) })
core.info( core.info(
`Updated pull request #${pull.number} (${headBranch} => ${inputs.base})` `Updated pull request #${pull.number} (${headBranch} => ${inputs.base})`
) )
return { return {
number: pull.number, number: pull.number,
html_url: pull.html_url, html_url: pull.html_url,
@ -139,13 +170,27 @@ export class GitHubHelper {
} }
async getRepositoryParent(headRepository: string): Promise<string | null> { async getRepositoryParent(headRepository: string): Promise<string | null> {
const {data: headRepo} = await this.octokit.rest.repos.get({ try {
...this.parseRepository(headRepository) const {data: headRepo} = await this.octokit.rest.repos.get({
}) ...this.parseRepository(headRepository)
if (!headRepo.parent) { })
return null
if (!headRepo.parent) {
return null
}
return headRepo.parent.full_name
} catch (error) {
// Gitea may not have the same parent repository structure
// Fall back to null if this fails
if (this.isGiteaInstance) {
core.warning(
`Unable to determine parent repository for ${headRepository}. This is expected for Gitea.`
)
return null
}
throw error
} }
return headRepo.parent.full_name
} }
async createOrUpdatePullRequest( async createOrUpdatePullRequest(
@ -169,6 +214,7 @@ export class GitHubHelper {
milestone: inputs.milestone milestone: inputs.milestone
}) })
} }
// Apply labels // Apply labels
if (inputs.labels.length > 0) { if (inputs.labels.length > 0) {
core.info(`Applying labels '${inputs.labels}'`) core.info(`Applying labels '${inputs.labels}'`)
@ -178,44 +224,76 @@ export class GitHubHelper {
labels: inputs.labels labels: inputs.labels
}) })
} }
// Apply assignees // Apply assignees
if (inputs.assignees.length > 0) { if (inputs.assignees.length > 0) {
core.info(`Applying assignees '${inputs.assignees}'`) core.info(`Applying assignees '${inputs.assignees}'`)
await this.octokit.rest.issues.addAssignees({ // Gitea has different assignee handling
...this.parseRepository(baseRepository), if (this.isGiteaInstance) {
issue_number: pull.number, try {
assignees: inputs.assignees for (const assignee of inputs.assignees) {
}) await this.octokit.request(
} 'POST /repos/{owner}/{repo}/issues/{issue_number}/assignees',
{
// Request reviewers and team reviewers ...this.parseRepository(baseRepository),
const requestReviewersParams = {} issue_number: pull.number,
if (inputs.reviewers.length > 0) { assignees: [assignee]
requestReviewersParams['reviewers'] = inputs.reviewers }
core.info(`Requesting reviewers '${inputs.reviewers}'`) )
} }
if (inputs.teamReviewers.length > 0) { } catch (error) {
const teams = utils.stripOrgPrefixFromTeams(inputs.teamReviewers) core.warning(
requestReviewersParams['team_reviewers'] = teams `Error assigning users in Gitea: ${utils.getErrorMessage(error)}`
core.info(`Requesting team reviewers '${teams}'`)
}
if (Object.keys(requestReviewersParams).length > 0) {
try {
await this.octokit.rest.pulls.requestReviewers({
...this.parseRepository(baseRepository),
pull_number: pull.number,
...requestReviewersParams
})
} catch (e) {
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
core.error(
`Unable to request reviewers. If requesting team reviewers a 'repo' scoped PAT is required.`
) )
} }
throw e } else {
// GitHub standard API
await this.octokit.rest.issues.addAssignees({
...this.parseRepository(baseRepository),
issue_number: pull.number,
assignees: inputs.assignees
})
} }
} }
// Skip reviewers functionality for Gitea as it might not be compatible
if (
!this.isGiteaInstance &&
(inputs.reviewers.length > 0 || inputs.teamReviewers.length > 0)
) {
const requestReviewersParams = {}
if (inputs.reviewers.length > 0) {
requestReviewersParams['reviewers'] = inputs.reviewers
core.info(`Requesting reviewers '${inputs.reviewers}'`)
}
if (inputs.teamReviewers.length > 0) {
const teams = utils.stripOrgPrefixFromTeams(inputs.teamReviewers)
requestReviewersParams['team_reviewers'] = teams
core.info(`Requesting team reviewers '${teams}'`)
}
if (Object.keys(requestReviewersParams).length > 0) {
try {
await this.octokit.rest.pulls.requestReviewers({
...this.parseRepository(baseRepository),
pull_number: pull.number,
...requestReviewersParams
})
} catch (e) {
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
core.error(
`Unable to request reviewers. If requesting team reviewers a 'repo' scoped PAT is required.`
)
}
throw e
}
}
} else if (
this.isGiteaInstance &&
(inputs.reviewers.length > 0 || inputs.teamReviewers.length > 0)
) {
core.warning('Reviewer assignment is not supported for Gitea instances')
}
return pull return pull
} }
@ -227,11 +305,32 @@ export class GitHubHelper {
branchRepository: string, branchRepository: string,
branch: string branch: string
): Promise<CommitResponse> { ): Promise<CommitResponse> {
// For Gitea, fall back to standard Git push if signed commits are not supported
if (this.isGiteaInstance) {
core.warning(
'Signed commits via API may not be fully supported in Gitea. Falling back to standard Git push.'
)
await git.push([
'--force-with-lease',
'origin',
`${branch}:refs/heads/${branch}`
])
// Return a simplified commit response
return {
sha: branchCommits[branchCommits.length - 1]?.sha || baseCommit.sha,
tree: branchCommits[branchCommits.length - 1]?.tree || baseCommit.tree,
verified: false
}
}
// Original GitHub implementation
let headCommit: CommitResponse = { let headCommit: CommitResponse = {
sha: baseCommit.sha, sha: baseCommit.sha,
tree: baseCommit.tree, tree: baseCommit.tree,
verified: false verified: false
} }
for (const commit of branchCommits) { for (const commit of branchCommits) {
headCommit = await this.createCommit( headCommit = await this.createCommit(
git, git,
@ -241,6 +340,7 @@ export class GitHubHelper {
branchRepository branchRepository
) )
} }
await this.createOrUpdateRef(branchRepository, branch, headCommit.sha) await this.createOrUpdateRef(branchRepository, branch, headCommit.sha)
return headCommit return headCommit
} }
@ -255,6 +355,7 @@ export class GitHubHelper {
const repository = this.parseRepository(branchRepository) const repository = this.parseRepository(branchRepository)
// In the case of an empty commit, the tree references the parent's tree // In the case of an empty commit, the tree references the parent's tree
let treeSha = parentCommit.tree let treeSha = parentCommit.tree
if (commit.changes.length > 0) { if (commit.changes.length > 0) {
core.info(`Creating tree objects for local commit ${commit.sha}`) core.info(`Creating tree objects for local commit ${commit.sha}`)
const treeObjects = await Promise.all( const treeObjects = await Promise.all(
@ -329,16 +430,29 @@ export class GitHubHelper {
tree: treeSha, tree: treeSha,
message: `${commit.subject}\n\n${commit.body}` message: `${commit.subject}\n\n${commit.body}`
}) })
core.info( core.info(
`Created commit ${remoteCommit.sha} for local commit ${commit.sha}` `Created commit ${remoteCommit.sha} for local commit ${commit.sha}`
) )
core.info(
`Commit verified: ${remoteCommit.verification.verified}; reason: ${remoteCommit.verification.reason}` // Gitea might not have the same verification structure
) let verified = false
if (
remoteCommit.verification &&
typeof remoteCommit.verification.verified !== 'undefined'
) {
verified = remoteCommit.verification.verified
core.info(
`Commit verified: ${verified}; reason: ${remoteCommit.verification.reason || 'unknown'}`
)
} else {
core.info('Commit verification information not available')
}
return { return {
sha: remoteCommit.sha, sha: remoteCommit.sha,
tree: remoteCommit.tree.sha, tree: remoteCommit.tree.sha,
verified: remoteCommit.verification.verified verified: verified
} }
} }
@ -347,14 +461,40 @@ export class GitHubHelper {
branchRepository: string branchRepository: string
): Promise<CommitResponse> { ): Promise<CommitResponse> {
const repository = this.parseRepository(branchRepository) const repository = this.parseRepository(branchRepository)
const {data: remoteCommit} = await this.octokit.rest.git.getCommit({
...repository, try {
commit_sha: sha const {data: remoteCommit} = await this.octokit.rest.git.getCommit({
}) ...repository,
return { commit_sha: sha
sha: remoteCommit.sha, })
tree: remoteCommit.tree.sha,
verified: remoteCommit.verification.verified // Handle different verification structure between GitHub and Gitea
let verified = false
if (
remoteCommit.verification &&
typeof remoteCommit.verification.verified !== 'undefined'
) {
verified = remoteCommit.verification.verified
}
return {
sha: remoteCommit.sha,
tree: remoteCommit.tree.sha,
verified: verified
}
} catch (error) {
if (this.isGiteaInstance) {
core.warning(
`Unable to get commit details from Gitea. This might be expected: ${utils.getErrorMessage(error)}`
)
// Return a placeholder response
return {
sha: sha,
tree: '', // We don't know the tree SHA
verified: false
}
}
throw error
} }
} }
@ -364,15 +504,18 @@ export class GitHubHelper {
newHead: string newHead: string
) { ) {
const repository = this.parseRepository(branchRepository) const repository = this.parseRepository(branchRepository)
const branchExists = await this.octokit.rest.repos
.getBranch({ // Check if branch exists
let branchExists = false
try {
await this.octokit.rest.repos.getBranch({
...repository, ...repository,
branch: branch branch: branch
}) })
.then( branchExists = true
() => true, } catch {
() => false branchExists = false
) }
if (branchExists) { if (branchExists) {
core.info(`Branch ${branch} exists; Updating ref`) core.info(`Branch ${branch} exists; Updating ref`)
@ -384,15 +527,28 @@ export class GitHubHelper {
}) })
} else { } else {
core.info(`Branch ${branch} does not exist; Creating ref`) core.info(`Branch ${branch} does not exist; Creating ref`)
await this.octokit.rest.git.createRef({ try {
...repository, await this.octokit.rest.git.createRef({
sha: newHead, ...repository,
ref: `refs/heads/${branch}` sha: newHead,
}) ref: `refs/heads/${branch}`
})
} catch (error) {
core.error(`Failed to create branch: ${utils.getErrorMessage(error)}`)
throw error
}
} }
} }
async convertToDraft(id: string): Promise<void> { async convertToDraft(id: string): Promise<void> {
// Skip for Gitea since GraphQL API likely isn't compatible
if (this.isGiteaInstance) {
core.warning(
'Draft pull requests are not supported in Gitea via the GraphQL API'
)
return
}
core.info(`Converting pull request to draft`) core.info(`Converting pull request to draft`)
await this.octokit.graphql({ await this.octokit.graphql({
query: `mutation($pullRequestId: ID!) { query: `mutation($pullRequestId: ID!) {

View File

@ -11,8 +11,30 @@ function getDraftInput(): {value: boolean; always: boolean} {
} }
} }
// Set Gitea instances from environment variable or input
function configureGiteaInstances() {
// First check if there's already an environment variable
if (!process.env.GITEA_INSTANCES) {
// If not, check if it was provided as input
const giteaInstancesInput = core.getInput('github-server-url')
if (giteaInstancesInput) {
core.info(
`Setting GITEA_INSTANCES environment variable to: ${giteaInstancesInput}`
)
process.env.GITEA_INSTANCES = giteaInstancesInput
}
}
if (process.env.GITEA_INSTANCES) {
core.info(`Configured Gitea instances: ${process.env.GITEA_INSTANCES}`)
}
}
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
// Configure Gitea instances before anything else
configureGiteaInstances()
const inputs: Inputs = { const inputs: Inputs = {
token: core.getInput('token'), token: core.getInput('token'),
branchToken: core.getInput('branch-token'), branchToken: core.getInput('branch-token'),
@ -44,9 +66,11 @@ async function run(): Promise<void> {
if (!inputs.token) { if (!inputs.token) {
throw new Error(`Input 'token' not supplied. Unable to continue.`) throw new Error(`Input 'token' not supplied. Unable to continue.`)
} }
if (!inputs.branchToken) { if (!inputs.branchToken) {
inputs.branchToken = inputs.token inputs.branchToken = inputs.token
} }
if (inputs.bodyPath) { if (inputs.bodyPath) {
if (!utils.fileExistsSync(inputs.bodyPath)) { if (!utils.fileExistsSync(inputs.bodyPath)) {
throw new Error(`File '${inputs.bodyPath}' does not exist.`) throw new Error(`File '${inputs.bodyPath}' does not exist.`)
@ -54,6 +78,7 @@ async function run(): Promise<void> {
// Update the body input with the contents of the file // Update the body input with the contents of the file
inputs.body = utils.readFile(inputs.bodyPath) inputs.body = utils.readFile(inputs.bodyPath)
} }
// 65536 characters is the maximum allowed for the pull request body. // 65536 characters is the maximum allowed for the pull request body.
if (inputs.body.length > 65536) { if (inputs.body.length > 65536) {
core.warning( core.warning(

View File

@ -38,3 +38,26 @@ function autoProxyAgent(octokit: OctokitCore) {
options.request.fetch = fetch options.request.fetch = fetch
}) })
} }
// Determine if a hostname is a Gitea instance
export function isGitea(hostname: string): boolean {
return process.env.GITEA_INSTANCES
? process.env.GITEA_INSTANCES.split(',').includes(hostname)
: false
}
// Get the API base URL for a given hostname
export function getApiBaseUrl(hostname: string): string {
// For GitHub, we'll use their standard API endpoint
if (hostname === 'github.com') {
return 'https://api.github.com'
}
// For Gitea, we need to modify the API path
if (isGitea(hostname)) {
return `https://${hostname}/api/v1`
}
// For GitHub Enterprise or other GitHub-compatible APIs
return `https://${hostname}/api/v3`
}