23 Commits
v1.4.0 ... main

Author SHA1 Message Date
Dennis Wielepsky
41e29eb9f1 Update README.md 2025-08-23 12:30:57 +02:00
Dennis Wielepsky
7cc8b666ee Merge pull request #33 from simonhammes/32-docker-hub-tags
Push tags to Docker Hub
2025-04-12 19:15:07 +02:00
simonhammes
0c8d330564 Update workflow name to avoid confusion 2025-04-09 20:26:55 +02:00
simonhammes
2a3ae27383 Push tags to Docker Hub
Closes #32
2025-04-09 20:26:34 +02:00
Dennis Wielepsky
a53ffa46ae Update Taskfile.yml 2025-04-07 21:08:21 +02:00
Dennis Wielepsky
63bcbe4034 Merge pull request #30 from arunavo4/main
[Feature] Starred Repo Mirror | Org Features | Mirroring Issues
2025-04-07 21:06:21 +02:00
ARUNAVO RAY
5c246b2e08 Update README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-06 13:03:36 +05:30
ARUNAVO RAY
469cbe3ef3 refactor Dockerfile to use multi-stage builds for improved efficiency
use multi-stage builds for improved efficiency
2025-04-02 21:03:09 +05:30
ARUNAVO RAY
182df9cdb0 Merge pull request #1 from arunavo4/private-repo-org
Features
2025-04-02 19:29:06 +05:30
Arunavo Ray
18572f38e2 add support for fetching private repositories from organizations using the search API 2025-04-02 19:27:40 +05:30
Arunavo Ray
0cac771ef0 add support for mirroring starred repositories to a dedicated Gitea organization and skip issues for starred repos 2025-04-02 17:07:39 +05:30
Arunavo Ray
a2f371c339 add support for starring repositories during mirroring process in Gitea 2025-04-02 16:41:14 +05:30
Arunavo Ray
688211311a add support for USE_SPECIFIC_USER feature to fetch starred repositories and organizations for a specified GitHub user 2025-04-02 16:12:58 +05:30
Arunavo Ray
4132707f86 add detailed testing for GitHub private repositories and organization access in debug script 2025-04-02 15:37:39 +05:30
Arunavo Ray
83f658cbf0 add debug script for testing Gitea and GitHub API access 2025-04-02 14:59:35 +05:30
Arunavo Ray
35e1b7e655 add support for including/excluding specific organizations and preserving organization structure during mirroring 2025-04-02 14:03:22 +05:30
Arunavo Ray
936246b9a7 add support for mirroring a single repository and to a specific Gitea organization 2025-04-02 13:57:54 +05:30
Arunavo Ray
b8f6c36fad add support for mirroring GitHub issues, starred repositories, and organization repositories 2025-04-02 13:49:50 +05:30
Dennis Wielepsky
16f2516f9f add possibility to retrieve private repos 2025-03-30 10:50:26 +02:00
Dennis Wielepsky
647a42f5cd update project configure 2025-03-30 10:11:17 +02:00
Dennis Wielepsky
56dda1aa88 add logger 2025-03-29 14:37:56 +01:00
Dennis Wielepsky
e641220e41 apply codestyle 2025-03-29 14:05:21 +01:00
Dennis Wielepsky
4c3ee18afa Add build for arm64 2024-11-10 13:36:17 +01:00
14 changed files with 1426 additions and 173 deletions

View File

@@ -22,6 +22,17 @@ jobs:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: metadata
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }}
# "latest" is automatically generated
# Docs: https://github.com/docker/metadata-action?tab=readme-ov-file#latest-tag
tags: |
type=semver,pattern={{version}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -35,8 +46,9 @@ jobs:
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
builder: ${{ steps.buildx.outputs.name }}
push: true
tags: ${{ secrets.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }}:latest
tags: ${{ steps.metadata.outputs.tags }}
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }}:buildcache,mode=max

View File

@@ -1,4 +1,4 @@
name: Docker Image CI
name: Tests
env:
IMAGE_NAME: mirror-to-gitea

View File

@@ -1,14 +1,63 @@
FROM node:lts-alpine
# Build stage
FROM node:lts-alpine AS builder
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install all dependencies (including dev dependencies needed for build)
RUN npm ci
COPY docker-entrypoint.sh .
COPY src ./src
# Copy source code
COPY --chown=node:node . .
RUN npm run build && rm -rf package*.json
# Build the application with minification
RUN npm run build -- --minify
# Prune dependencies stage
FROM node:lts-alpine AS deps
WORKDIR /app
# Copy package files
COPY --from=builder /app/package.json /app/package-lock.json ./
# Install production dependencies only
RUN npm install --omit=dev --production && \
# Remove unnecessary npm cache and temp files to reduce size
npm cache clean --force && \
rm -rf /tmp/* /var/cache/apk/*
# Production stage
FROM node:lts-alpine AS production
# Add Docker Alpine packages and remove cache in the same layer
RUN apk --no-cache add ca-certificates tini && \
rm -rf /var/cache/apk/*
# Set non-root user for better security
USER node
# Set working directory owned by node user
WORKDIR /app
# Copy only the built application and entry point from builder
COPY --from=builder --chown=node:node /app/dist ./dist
COPY --from=builder --chown=node:node /app/docker-entrypoint.sh .
# Copy only production node_modules
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
# Make entry point executable
RUN chmod +x /app/docker-entrypoint.sh
# Set environment to production to disable development features
ENV NODE_ENV=production
# Use tini as init system to properly handle signals
ENTRYPOINT ["/sbin/tini", "--"]
# The command to run
CMD [ "/app/docker-entrypoint.sh" ]

135
README.md
View File

@@ -1,5 +1,10 @@
# Automatically Mirror Github Repo To Your Gitea Server
This project is considered in maintenance mode and will only receive bug-fixes but likely no new features.
A potential successor may be [Gitea-Mirror](https://github.com/RayLabsHQ/gitea-mirror).
If you are interested in taking over the project, feel free to reach out to me.
## Badges
[![image pulls](https://img.shields.io/docker/pulls/jaedle/mirror-to-gitea.svg)](https://cloud.docker.com/repository/docker/jaedle/mirror-to-gitea)
@@ -21,6 +26,15 @@ The mirror settings are default by your gitea instance.
It is also possible to mirror private repos, which can be configred here in [#parameters](#parameters). When mirroring
private repos, they will be created as private repos on your gitea server.
Additionally, you can now mirror:
- Issues from GitHub repositories (including labels)
- Starred repositories from your GitHub account
- Repositories from organizations you belong to
- Filter which organizations to include or exclude
- Maintain original organization structure in Gitea
- A single repository instead of all repositories
- Repositories to a specific Gitea organization
## Prerequisites
- A github user or organization with repositories
@@ -39,10 +53,22 @@ All configuration is performed through environment variables. Flags are consider
| GITHUB_USERNAME | yes | string | - | The name of the GitHub user or organisation to mirror. |
| GITEA_URL | yes | string | - | The url of your Gitea server. |
| GITEA_TOKEN | yes | string | - | The token for your gitea user (Settings -> Applications -> Generate New Token). **Attention: if this is set, the token will be transmitted to your specified Gitea instance!** |
| GITHUB_TOKEN | no* | string | - | GitHub token (PAT). Is mandatory in combination with `MIRROR_PRIVATE_REPOSITORIES`. |
| GITHUB_TOKEN | no* | string | - | GitHub token (PAT). Is mandatory in combination with `MIRROR_PRIVATE_REPOSITORIES`, `MIRROR_ISSUES`, `MIRROR_STARRED`, `MIRROR_ORGANIZATIONS`, or `SINGLE_REPO`. |
| MIRROR_PRIVATE_REPOSITORIES | no | bool | FALSE | If set to `true` your private GitHub Repositories will be mirrored to Gitea. Requires `GITHUB_TOKEN`. |
| MIRROR_ISSUES | no | bool | FALSE | If set to `true` the issues of your GitHub repositories will be mirrored to Gitea. Requires `GITHUB_TOKEN`. |
| MIRROR_STARRED | no | bool | FALSE | If set to `true` repositories you've starred on GitHub will be mirrored to Gitea. Requires `GITHUB_TOKEN`. |
| MIRROR_ORGANIZATIONS | no | bool | FALSE | If set to `true` repositories from organizations you belong to will be mirrored to Gitea. Requires `GITHUB_TOKEN`. |
| USE_SPECIFIC_USER | no | bool | FALSE | If set to `true`, the tool will use public API endpoints to fetch starred repositories and organizations for the specified `GITHUB_USERNAME` instead of the authenticated user. |
| INCLUDE_ORGS | no | string | "" | Comma-separated list of GitHub organization names to include when mirroring organizations. If not specified, all organizations will be included. |
| EXCLUDE_ORGS | no | string | "" | Comma-separated list of GitHub organization names to exclude when mirroring organizations. Takes precedence over `INCLUDE_ORGS`. |
| PRESERVE_ORG_STRUCTURE | no | bool | FALSE | If set to `true`, each GitHub organization will be mirrored to a Gitea organization with the same name. If the organization doesn't exist, it will be created. |
| SINGLE_REPO | no | string | - | URL of a single GitHub repository to mirror (e.g., https://github.com/username/repo or username/repo). When specified, only this repository will be mirrored. Requires `GITHUB_TOKEN`. |
| GITEA_ORGANIZATION | no | string | - | Name of a Gitea organization to mirror repositories to. If doesn't exist, will be created. |
| GITEA_ORG_VISIBILITY | no | string | public | Visibility of the Gitea organization to create. Can be "public" or "private". |
| GITEA_STARRED_ORGANIZATION | no | string | github | Name of a Gitea organization to mirror starred repositories to. If doesn't exist, will be created. Defaults to "github". |
| SKIP_STARRED_ISSUES | no | bool | FALSE | If set to `true` will not mirror issues for starred repositories, even if `MIRROR_ISSUES` is enabled. |
| SKIP_FORKS | no | bool | FALSE | If set to `true` will disable the mirroring of forks from your GitHub User / Organisation. |
| DELAY | no | int | 3600 | Number of seconds between program executions. Setting this will only affect how soon after a new repo was created a mirror may appar on Gitea, but has no affect on the ongoing replication. |
| DELAY | no | int | 3600 | Number of seconds between program executions. Setting this will only affect how soon after a new repo was created a mirror may appear on Gitea, but has no effect on the ongoing replication. |
| DRY_RUN | no | bool | FALSE | If set to `true` will perform no writing changes to your Gitea instance, but log the planned actions. |
| INCLUDE | no | string | "*" | Name based repository filter (include): If any filter matches, the repository will be mirrored. It supports glob format, multiple filters can be separated with commas (`,`) |
| EXCLUDE | no | string | "" | Name based repository filter (exclude). If any filter matches, the repository will not be mirrored. It supports glob format, multiple filters can be separated with commas (`,`). `EXCLUDE` filters are applied after `INCLUDE` ones.
@@ -57,11 +83,90 @@ docker container run \
-e GITHUB_USERNAME=github-user \
-e GITEA_URL=https://your-gitea.url \
-e GITEA_TOKEN=please-exchange-with-token \
-e GITHUB_TOKEN=your-github-token \
-e MIRROR_ISSUES=true \
-e MIRROR_STARRED=true \
-e MIRROR_ORGANIZATIONS=true \
jaedle/mirror-to-gitea:latest
```
This will a spin up a docker container which will run forever, mirroring all your repositories once every hour to your
gitea server.
### Mirror Only Specific Organizations
```sh
docker container run \
-d \
--restart always \
-e GITHUB_USERNAME=github-user \
-e GITEA_URL=https://your-gitea.url \
-e GITEA_TOKEN=please-exchange-with-token \
-e GITHUB_TOKEN=your-github-token \
-e MIRROR_ORGANIZATIONS=true \
-e INCLUDE_ORGS=org1,org2,org3 \
jaedle/mirror-to-gitea:latest
```
### Mirror Organizations with Preserved Structure
```sh
docker container run \
-d \
--restart always \
-e GITHUB_USERNAME=github-user \
-e GITEA_URL=https://your-gitea.url \
-e GITEA_TOKEN=please-exchange-with-token \
-e GITHUB_TOKEN=your-github-token \
-e MIRROR_ORGANIZATIONS=true \
-e PRESERVE_ORG_STRUCTURE=true \
-e GITEA_ORG_VISIBILITY=private \
jaedle/mirror-to-gitea:latest
```
### Mirror a Single Repository
```sh
docker container run \
-d \
--restart always \
-e GITHUB_USERNAME=github-user \
-e GITEA_URL=https://your-gitea.url \
-e GITEA_TOKEN=please-exchange-with-token \
-e GITHUB_TOKEN=your-github-token \
-e SINGLE_REPO=https://github.com/organization/repository \
jaedle/mirror-to-gitea:latest
```
### Mirror to a Gitea Organization
```sh
docker container run \
-d \
--restart always \
-e GITHUB_USERNAME=github-user \
-e GITEA_URL=https://your-gitea.url \
-e GITEA_TOKEN=please-exchange-with-token \
-e GITHUB_TOKEN=your-github-token \
-e GITEA_ORGANIZATION=my-organization \
-e GITEA_ORG_VISIBILITY=private \
jaedle/mirror-to-gitea:latest
```
### Mirror Starred Repositories to a Dedicated Organization
```sh
docker container run \
-d \
--restart always \
-e GITHUB_USERNAME=github-user \
-e GITEA_URL=https://your-gitea.url \
-e GITEA_TOKEN=please-exchange-with-token \
-e GITHUB_TOKEN=your-github-token \
-e MIRROR_STARRED=true \
-e GITEA_STARRED_ORGANIZATION=github \
-e SKIP_STARRED_ISSUES=true \
jaedle/mirror-to-gitea:latest
```
This configuration will mirror all starred repositories to a Gitea organization named "github" and will not mirror issues for these starred repositories.
### Docker Compose
@@ -76,6 +181,18 @@ services:
- GITHUB_USERNAME=github-user
- GITEA_URL=https://your-gitea.url
- GITEA_TOKEN=please-exchange-with-token
- GITHUB_TOKEN=your-github-token
- MIRROR_ISSUES=true
- MIRROR_STARRED=true
- MIRROR_ORGANIZATIONS=true
# Organization specific options
# - INCLUDE_ORGS=org1,org2
# - EXCLUDE_ORGS=org3,org4
# - PRESERVE_ORG_STRUCTURE=true
# Other options
# - SINGLE_REPO=https://github.com/organization/repository
# - GITEA_ORGANIZATION=my-organization
# - GITEA_ORG_VISIBILITY=public
```
## Development
@@ -100,6 +217,16 @@ Create `.secrets.rc` containing at least the following variables:
export GITHUB_USERNAME='...'
export GITHUB_TOKEN='...'
export GITEA_URL='...'
export GITEA_TOKEN='...'
export MIRROR_ISSUES='true'
export MIRROR_STARRED='true'
export MIRROR_ORGANIZATIONS='true'
# export INCLUDE_ORGS='org1,org2'
# export EXCLUDE_ORGS='org3,org4'
# export PRESERVE_ORG_STRUCTURE='true'
# export SINGLE_REPO='https://github.com/user/repo'
# export GITEA_ORGANIZATION='my-organization'
# export GITEA_ORG_VISIBILITY='public'
```
Execute the script in foreground:

View File

@@ -6,7 +6,7 @@ tasks:
cmds:
- task: install
- task: clean
- task: check
# - task: check
- task: test
- task: build
@@ -24,4 +24,4 @@ tasks:
clean: npm run clean
check: npm run check
test: npm run test
build: npm run build
build: npm run build

166
debug.sh Executable file
View File

@@ -0,0 +1,166 @@
#!/usr/bin/env bash
set -e
echo "Mirror to Gitea Debug Script"
echo "============================="
source .secrets.rc
# Get host machine IP address
HOST_IP=$(ipconfig getifaddr en0)
echo "Host IP: $HOST_IP"
GITEA_URL_HOST=${GITEA_URL/localhost/$HOST_IP}
echo "Gitea URL: $GITEA_URL"
echo "Gitea URL for Docker: $GITEA_URL_HOST"
echo -e "\nTesting Gitea API access directly:"
curl -s -H "Authorization: token $GITEA_TOKEN" "$GITEA_URL/api/v1/user" | jq '.'
echo -e "\nTesting GitHub token validity:"
GITHUB_USER_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user")
echo "$GITHUB_USER_RESPONSE" | jq '. | {login, name}'
# Testing access to private repositories
echo -e "\nTesting GitHub private repositories access:"
PRIVATE_REPOS_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user/repos?visibility=private&per_page=1")
PRIVATE_REPOS_COUNT=$(echo "$PRIVATE_REPOS_RESPONSE" | jq '. | length')
if [ "$PRIVATE_REPOS_COUNT" -eq 0 ]; then
echo "No private repositories found or no permission to access them."
else
echo "Found private repositories. First one: $(echo "$PRIVATE_REPOS_RESPONSE" | jq '.[0].full_name')"
fi
echo -e "\nTesting GitHub organization access:"
echo "Method 1 - Using /user/orgs endpoint (authenticated user):"
ORG_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user/orgs")
ORG_COUNT=$(echo "$ORG_RESPONSE" | jq '. | length')
if [ "$ORG_COUNT" -eq 0 ]; then
echo "No organizations found via /user/orgs endpoint."
else
echo "Organizations found via /user/orgs:"
echo "$ORG_RESPONSE" | jq '.[].login'
fi
echo -e "\nMethod 2 - Using /users/{username}/orgs endpoint (specific user):"
PUBLIC_USER_ORGS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/users/$GITHUB_USERNAME/orgs")
PUBLIC_ORG_COUNT=$(echo "$PUBLIC_USER_ORGS" | jq '. | length')
if [ "$PUBLIC_ORG_COUNT" -eq 0 ]; then
echo "No public organizations found for $GITHUB_USERNAME via /users/{username}/orgs endpoint."
else
echo "Public organizations found for $GITHUB_USERNAME:"
echo "$PUBLIC_USER_ORGS" | jq '.[].login'
fi
echo "Method 3 - Looking for specific organizations:"
for org in "Gameplex-labs" "Neucruit" "uiastra"; do
ORG_DETAILS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/orgs/$org")
if [[ $(echo "$ORG_DETAILS" | jq 'has("login")') == "true" ]]; then
echo "Found organization: $org"
# Check if we can access the organization's repositories
ORG_REPOS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/orgs/$org/repos?per_page=1")
REPO_COUNT=$(echo "$ORG_REPOS" | jq '. | length')
if [ "$REPO_COUNT" -gt 0 ]; then
echo " Can access repositories for $org"
echo " Example repo: $(echo "$ORG_REPOS" | jq '.[0].full_name')"
else
echo " Cannot access repositories for $org or organization has no repositories"
fi
else
echo "Could not find organization: $org (or no permission to access it)"
fi
done
echo -e "\nTesting GitHub starred repos access:"
echo "Method 1 - Using /user/starred endpoint (authenticated user):"
STARRED_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user/starred?per_page=1")
STARRED_COUNT=$(echo "$STARRED_RESPONSE" | jq '. | length')
if [ "$STARRED_COUNT" -eq 0 ]; then
echo "No starred repositories found. You may not have starred any GitHub repositories."
else
echo "First starred repo: $(echo "$STARRED_RESPONSE" | jq '.[0].full_name')"
echo "Total starred repositories accessible: $(curl -s -H "Authorization: token $GITHUB_TOKEN" -I "https://api.github.com/user/starred" | grep -i "^link:" | grep -o "page=[0-9]*" | sort -r | head -1 | cut -d= -f2 || echo "Unknown")"
fi
echo -e "\nMethod 2 - Using /users/{username}/starred endpoint (specific user):"
PUBLIC_STARRED_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/users/$GITHUB_USERNAME/starred?per_page=1")
PUBLIC_STARRED_COUNT=$(echo "$PUBLIC_STARRED_RESPONSE" | jq '. | length')
if [ "$PUBLIC_STARRED_COUNT" -eq 0 ]; then
echo "No public starred repositories found for $GITHUB_USERNAME."
else
echo "First public starred repo for $GITHUB_USERNAME: $(echo "$PUBLIC_STARRED_RESPONSE" | jq '.[0].full_name')"
echo "Total public starred repositories for $GITHUB_USERNAME: $(curl -s -H "Authorization: token $GITHUB_TOKEN" -H "X-GitHub-Api-Version: 2022-11-28" -I "https://api.github.com/users/$GITHUB_USERNAME/starred" | grep -i "^link:" | grep -o "page=[0-9]*" | sort -r | head -1 | cut -d= -f2 || echo "Unknown")"
fi
echo -e "\nTesting GitHub issues access:"
echo "Checking for issues in your repositories..."
# Get a list of repositories to check for issues
USER_REPOS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user/repos?per_page=5&sort=updated")
REPO_COUNT=$(echo "$USER_REPOS" | jq '. | length')
echo "Found $REPO_COUNT recently updated repositories to check for issues"
# Check each repository for issues
for i in $(seq 0 $(($REPO_COUNT - 1))); do
REPO=$(echo "$USER_REPOS" | jq -r ".[$i].full_name")
REPO_HAS_ISSUES=$(echo "$USER_REPOS" | jq -r ".[$i].has_issues")
if [ "$REPO_HAS_ISSUES" = "true" ]; then
ISSUES_RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/$REPO/issues?state=all&per_page=1")
ISSUES_COUNT=$(curl -s -H "Authorization: token $GITHUB_TOKEN" -I "https://api.github.com/repos/$REPO/issues?state=all" | grep -i "^link:" | grep -o "page=[0-9]*" | sort -r | head -1 | cut -d= -f2 || echo "0")
if [ -z "$ISSUES_COUNT" ]; then
# If we couldn't get the count from Link header, count the array length
ISSUES_COUNT=$(echo "$ISSUES_RESPONSE" | jq '. | length')
fi
if [ "$ISSUES_COUNT" -gt 0 ]; then
echo "Repository $REPO has approximately $ISSUES_COUNT issues"
echo "Latest issue: $(echo "$ISSUES_RESPONSE" | jq -r '.[0].title // "No title"')"
else
echo "Repository $REPO has issues enabled but no issues were found"
fi
else
echo "Repository $REPO has issues disabled"
fi
done
echo -e "\nVerifying GitHub token scopes for issues access:"
SCOPES=$(curl -s -I -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user" | grep -i "^x-oauth-scopes:" | cut -d ":" -f 2- | tr -d '\r' || echo "None found")
if [[ "$SCOPES" == *"repo"* ]]; then
echo "Your token has the 'repo' scope, which is required for issues access"
else
echo "WARNING: Your token may not have the 'repo' scope, which is required for full issues access"
echo "Testing issues access directly: $(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/$GITHUB_USERNAME/$(echo "$USER_REPOS" | jq -r '.[0].name')/issues")"
fi
echo -e "\nVerifying GitHub token scopes:"
SCOPES=$(curl -s -I -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user" | grep -i "^x-oauth-scopes:" | cut -d ":" -f 2- | tr -d '\r' || echo "None found")
if [ -z "$SCOPES" ] || [ "$SCOPES" = "None found" ]; then
echo "No explicit scopes found in GitHub token. This may be a personal access token (classic) with default scopes."
echo "Testing functionality directly:"
echo "- Can access user info: $(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user")"
echo "- Can access private repos: $(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user/repos?visibility=private&per_page=1")"
echo "- Can access organizations: $(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user/orgs")"
echo "- Can access starred repos: $(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user/starred?per_page=1")"
else
echo "Your token has these scopes: $SCOPES"
fi
echo -e "\nRequired scopes for full functionality:"
echo "- repo (for repositories and issues)"
echo "- read:org (for organization access)"
echo "- user (for starred repositories)"
echo -e "\nSpecific user mode tests (USE_SPECIFIC_USER=true):"
echo "This mode uses the following endpoints:"
echo "- GET /users/{username}/orgs"
echo "- GET /users/{username}/starred"
echo "These endpoints are working: $([ "$PUBLIC_ORG_COUNT" -ge 0 ] && [ "$PUBLIC_STARRED_COUNT" -ge 0 ] && echo "YES" || echo "NO")"
echo -e "\nYour environment should now be ready for testing."
echo "To test with the new USE_SPECIFIC_USER feature:"
echo "export USE_SPECIFIC_USER=true"
echo "Run ./run-local.sh to start the mirroring process."

219
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@types/jest": "^29.5.13",
"minimatch": "^10.0.1",
"p-queue": "^8.0.1",
"pino": "^9.6.0",
"superagent": "^10.1.0"
},
"devDependencies": {
@@ -2039,6 +2040,15 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -2965,6 +2975,15 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
@@ -4481,6 +4500,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"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",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -4674,6 +4702,43 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pino": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz",
"integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^4.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT"
},
"node_modules/pirates": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -4719,6 +4784,22 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/process-warning": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
"integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -4804,6 +4885,12 @@
}
]
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -4833,6 +4920,15 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -4950,6 +5046,15 @@
}
]
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -5134,6 +5239,15 @@
"node": ">=8"
}
},
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -5153,6 +5267,15 @@
"source-map": "^0.6.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -5327,6 +5450,15 @@
"node": "*"
}
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -6985,6 +7117,11 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="
},
"babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -7676,6 +7813,11 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true
},
"fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="
},
"fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
@@ -8804,6 +8946,11 @@
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
},
"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",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="
},
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -8936,6 +9083,37 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
},
"pino": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz",
"integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==",
"requires": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^4.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
}
},
"pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"requires": {
"split2": "^4.0.0"
}
},
"pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="
},
"pirates": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -8968,6 +9146,11 @@
}
}
},
"process-warning": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
"integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="
},
"prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -9014,6 +9197,11 @@
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true
},
"quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -9037,6 +9225,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
},
"real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -9101,6 +9294,11 @@
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -9249,6 +9447,14 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="
},
"sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"requires": {
"atomic-sleep": "^1.0.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -9265,6 +9471,11 @@
"source-map": "^0.6.0"
}
},
"split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -9394,6 +9605,14 @@
}
}
},
"thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"requires": {
"real-require": "^0.2.0"
}
},
"tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",

View File

@@ -14,8 +14,8 @@
"url": "git+https://github.com/jaedle/mirror-to-gitea.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"author": "Dennis Wielepsky",
"license": "MIT",
"bugs": {
"url": "https://github.com/jaedle/mirror-to-gitea/issues"
},
@@ -27,6 +27,7 @@
"@types/jest": "^29.5.13",
"minimatch": "^10.0.1",
"p-queue": "^8.0.1",
"pino": "^9.6.0",
"superagent": "^10.1.0"
},
"devDependencies": {

View File

@@ -5,12 +5,28 @@ set -ex
docker image build -t jaedle/mirror-to-gitea:development .
source .secrets.rc
# Get host IP for Mac to connect to local Gitea instance
HOST_IP=$(ipconfig getifaddr en0)
echo "Using host IP for local Gitea: $HOST_IP"
GITEA_URL_DOCKER=${GITEA_URL/localhost/$HOST_IP}
echo "Gitea URL for Docker: $GITEA_URL_DOCKER"
docker container run \
-it \
--rm \
-e GITHUB_USERNAME="$GITHUB_USERNAME" \
-e GITEA_URL="$GITEA_URL" \
-e GITEA_URL="$GITEA_URL_DOCKER" \
-e GITEA_TOKEN="$GITEA_TOKEN" \
-e GITHUB_TOKEN="$GITHUB_TOKEN" \
-e MIRROR_PRIVATE_REPOSITORIES="true" \
-e MIRROR_ISSUES="true" \
-e MIRROR_STARRED="true" \
-e MIRROR_ORGANIZATIONS="true" \
-e USE_SPECIFIC_USER="$USE_SPECIFIC_USER" \
-e INCLUDE_ORGS="$INCLUDE_ORGS" \
-e EXCLUDE_ORGS="$EXCLUDE_ORGS" \
-e PRESERVE_ORG_STRUCTURE="$PRESERVE_ORG_STRUCTURE" \
-e GITEA_STARRED_ORGANIZATION="$GITEA_STARRED_ORGANIZATION" \
-e SKIP_STARRED_ISSUES="$SKIP_STARRED_ISSUES" \
-e DRY_RUN="true" \
jaedle/mirror-to-gitea:development

View File

@@ -35,15 +35,37 @@ export function configuration() {
token: process.env.GITHUB_TOKEN,
skipForks: readBoolean("SKIP_FORKS"),
privateRepositories: readBoolean("MIRROR_PRIVATE_REPOSITORIES"),
mirrorIssues: readBoolean("MIRROR_ISSUES"),
mirrorStarred: readBoolean("MIRROR_STARRED"),
mirrorOrganizations: readBoolean("MIRROR_ORGANIZATIONS"),
useSpecificUser: readBoolean("USE_SPECIFIC_USER"),
singleRepo: readEnv("SINGLE_REPO"),
includeOrgs: (readEnv("INCLUDE_ORGS") || "")
.split(",")
.map((o) => o.trim())
.filter((o) => o.length > 0),
excludeOrgs: (readEnv("EXCLUDE_ORGS") || "")
.split(",")
.map((o) => o.trim())
.filter((o) => o.length > 0),
preserveOrgStructure: readBoolean("PRESERVE_ORG_STRUCTURE"),
skipStarredIssues: readBoolean("SKIP_STARRED_ISSUES"),
},
gitea: {
url: mustReadEnv("GITEA_URL"),
token: mustReadEnv("GITEA_TOKEN"),
organization: readEnv("GITEA_ORGANIZATION"),
visibility: readEnv("GITEA_ORG_VISIBILITY") || "public",
starredReposOrg: readEnv("GITEA_STARRED_ORGANIZATION") || "github",
},
dryRun: readBoolean("DRY_RUN"),
delay: readInt("DELAY") ?? defaultDelay,
include: (readEnv("INCLUDE") ?? defaultInclude).split(",").map(f => f.trim()),
exclude: (readEnv("EXCLUDE") ?? defaultExclude).split(",").map(f => f.trim()),
include: (readEnv("INCLUDE") ?? defaultInclude)
.split(",")
.map((f) => f.trim()),
exclude: (readEnv("EXCLUDE") ?? defaultExclude)
.split(",")
.map((f) => f.trim()),
singleRun: readBoolean("SINGLE_RUN"),
};
@@ -53,5 +75,13 @@ export function configuration() {
);
}
// GitHub token is required for mirroring issues, starred repos, and orgs
if ((config.github.mirrorIssues || config.github.mirrorStarred || config.github.mirrorOrganizations || config.github.singleRepo)
&& config.github.token === undefined) {
throw new Error(
"invalid configuration, mirroring issues, starred repositories, organizations, or a single repo requires setting GITHUB_TOKEN",
);
}
return config;
}

View File

@@ -1,23 +1,263 @@
async function getRepositories(octokit, mirrorOptions) {
const repos = await octokit
.paginate("GET /users/:username/repos", { username: "jaedle" })
.then(toRepositoryList);
if (mirrorOptions.skipForks) {
return repos.filter((repo) => !repo.fork);
let repositories = [];
// Check if we're mirroring a single repo
if (mirrorOptions.singleRepo) {
const singleRepo = await fetchSingleRepository(octokit, mirrorOptions.singleRepo);
if (singleRepo) {
repositories.push(singleRepo);
}
} else {
// Standard mirroring logic
const publicRepositories = await fetchPublicRepositories(
octokit,
mirrorOptions.username,
);
const privateRepos = mirrorOptions.privateRepositories
? await fetchPrivateRepositories(octokit)
: [];
// Fetch starred repos if the option is enabled
const starredRepos = mirrorOptions.mirrorStarred
? await fetchStarredRepositories(octokit, {
username: mirrorOptions.useSpecificUser ? mirrorOptions.username : undefined
})
: [];
// Fetch organization repos if the option is enabled
const orgRepos = mirrorOptions.mirrorOrganizations
? await fetchOrganizationRepositories(
octokit,
mirrorOptions.includeOrgs,
mirrorOptions.excludeOrgs,
mirrorOptions.preserveOrgStructure,
{
username: mirrorOptions.useSpecificUser ? mirrorOptions.username : undefined,
privateRepositories: mirrorOptions.privateRepositories
}
)
: [];
// Combine all repositories and filter duplicates
repositories = filterDuplicates([
...publicRepositories,
...privateRepos,
...starredRepos,
...orgRepos
]);
}
return repos;
return mirrorOptions.skipForks ? withoutForks(repositories) : repositories;
}
function toRepositoryList(repositories) {
return repositories.map((repository) => {
async function fetchSingleRepository(octokit, repoUrl) {
try {
// Remove URL prefix if present and clean up
let repoPath = repoUrl;
if (repoPath.startsWith('https://github.com/')) {
repoPath = repoPath.replace('https://github.com/', '');
}
if (repoPath.endsWith('.git')) {
repoPath = repoPath.slice(0, -4);
}
// Split into owner and repo
const [owner, repo] = repoPath.split('/');
if (!owner || !repo) {
console.error(`Invalid repository URL format: ${repoUrl}`);
return null;
}
// Fetch the repository details
const response = await octokit.rest.repos.get({
owner,
repo
});
return {
name: response.data.name,
url: response.data.clone_url,
private: response.data.private,
fork: response.data.fork,
owner: response.data.owner.login,
full_name: response.data.full_name,
has_issues: response.data.has_issues,
};
} catch (error) {
console.error(`Error fetching single repository ${repoUrl}:`, error.message);
return null;
}
}
async function fetchPublicRepositories(octokit, username) {
return octokit
.paginate("GET /users/:username/repos", { username })
.then(toRepositoryList);
}
async function fetchPrivateRepositories(octokit) {
return octokit
.paginate("GET /user/repos", {
affiliation: "owner",
visibility: "private",
})
.then(toRepositoryList);
}
async function fetchStarredRepositories(octokit, options = {}) {
// If a specific username is provided, use the user-specific endpoint
if (options.username) {
return octokit
.paginate("GET /users/{username}/starred", {
username: options.username,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
})
.then(repos => toRepositoryList(repos.map(repo => ({...repo, starred: true}))));
}
// Default: Get starred repos for the authenticated user (what was previously used)
return octokit
.paginate("GET /user/starred")
.then(repos => toRepositoryList(repos.map(repo => ({...repo, starred: true}))));
}
async function fetchOrganizationRepositories(octokit, includeOrgs = [], excludeOrgs = [], preserveOrgStructure = false, options = {}) {
try {
// Get all organizations the user belongs to
let allOrgs;
// If a specific username is provided, use the user-specific endpoint
if (options.username) {
allOrgs = await octokit.paginate("GET /users/{username}/orgs", {
username: options.username,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
} else {
// Default: Get organizations for the authenticated user (what was previously used)
allOrgs = await octokit.paginate("GET /user/orgs");
}
// Filter organizations based on include/exclude lists
let orgsToProcess = allOrgs;
if (includeOrgs.length > 0) {
// Only include specific organizations
orgsToProcess = orgsToProcess.filter(org =>
includeOrgs.includes(org.login)
);
}
if (excludeOrgs.length > 0) {
// Exclude specific organizations
orgsToProcess = orgsToProcess.filter(org =>
!excludeOrgs.includes(org.login)
);
}
console.log(`Processing repositories from ${orgsToProcess.length} organizations`);
// Determine if we need to fetch private repositories
const privateRepoAccess = options.privateRepositories && octokit.auth;
const allOrgRepos = [];
// Process each organization
for (const org of orgsToProcess) {
const orgName = org.login;
console.log(`Fetching repositories for organization: ${orgName}`);
try {
let orgRepos = [];
// Use search API for organizations when private repositories are requested
// This is based on the GitHub community discussion recommendation
if (privateRepoAccess) {
console.log(`Using search API to fetch both public and private repositories for org: ${orgName}`);
// Query for both public and private repositories in the organization
const searchQuery = `org:${orgName}`;
const searchResults = await octokit.paginate("GET /search/repositories", {
q: searchQuery,
per_page: 100
});
// Search API returns repositories in the 'items' array
orgRepos = searchResults.flatMap(result => result.items || []);
console.log(`Found ${orgRepos.length} repositories (public and private) for org: ${orgName}`);
} else {
// Use standard API for public repositories only
orgRepos = await octokit.paginate("GET /orgs/{org}/repos", {
org: orgName
});
console.log(`Found ${orgRepos.length} public repositories for org: ${orgName}`);
}
// Add organization context to each repository if preserveOrgStructure is enabled
if (preserveOrgStructure) {
orgRepos = orgRepos.map(repo => ({
...repo,
organization: orgName
}));
}
allOrgRepos.push(...orgRepos);
} catch (orgError) {
console.error(`Error fetching repositories for org ${orgName}:`, orgError.message);
}
}
// Convert to repository list format
return toRepositoryList(allOrgRepos, preserveOrgStructure);
} catch (error) {
console.error("Error fetching organization repositories:", error.message);
return [];
}
}
function withoutForks(repositories) {
return repositories.filter((repo) => !repo.fork);
}
function filterDuplicates(repositories) {
const unique = [];
const seen = new Set();
for (const repo of repositories) {
if (!seen.has(repo.url)) {
seen.add(repo.url);
unique.push(repo);
}
}
return unique;
}
function toRepositoryList(repositories, preserveOrgStructure = false) {
return repositories.map((repository) => {
const repoInfo = {
name: repository.name,
url: repository.clone_url,
private: repository.private,
fork: repository.fork,
owner: repository.owner?.login,
full_name: repository.full_name,
has_issues: repository.has_issues,
};
// Add organization context if it exists and preserveOrgStructure is enabled
if (preserveOrgStructure && repository.organization) {
repoInfo.organization = repository.organization;
}
// Preserve starred status if present
if (repository.starred) {
repoInfo.starred = true;
}
return repoInfo;
});
}

View File

@@ -79,4 +79,86 @@ describe("get-github-repositories", () => {
{ name: "repo3", url: "clone-url-of-repo3", private: false, fork: false },
]);
});
it("includes private repositories for user", async () => {
const moctokit = new Moctokit();
moctokit.rest.repos.listForUser({ username: "jaedle" }).reply({
status: 200,
data: [
{
name: "public-repo-1",
clone_url: "clone-url-of-public-repo-1",
private: false,
fork: false,
},
{
name: "public-repo-2",
clone_url: "clone-url-of-public-repo-2",
private: false,
fork: false,
},
],
});
moctokit.rest.repos
.listForAuthenticatedUser({
affiliation: "owner",
visibility: "private",
})
.reply({
status: 200,
data: [
{
name: "private-repo-1",
clone_url: "clone-url-of-private-repo-1",
private: true,
fork: false,
},
{
name: "private-repo-2",
clone_url: "clone-url-of-private-repo-2",
private: true,
fork: false,
},
],
});
const result = await get(
new Octokit({
auth: "a-github-token",
}),
{
username: "jaedle",
privateRepositories: true,
skipForks: false,
},
);
expect(result).toEqual([
{
name: "public-repo-1",
url: "clone-url-of-public-repo-1",
private: false,
fork: false,
},
{
name: "public-repo-2",
url: "clone-url-of-public-repo-2",
private: false,
fork: false,
},
{
name: "private-repo-1",
url: "clone-url-of-private-repo-1",
private: true,
fork: false,
},
{
name: "private-repo-2",
url: "clone-url-of-private-repo-2",
private: true,
fork: false,
},
]);
});
});

View File

@@ -3,128 +3,8 @@ import { minimatch } from "minimatch";
import PQueue from "p-queue";
import request from "superagent";
import { configuration } from "./configuration.mjs";
async function getGithubRepositories(
username,
token,
mirrorPrivateRepositories,
mirrorForks,
include,
exclude,
) {
const octokit = new Octokit({
auth: token || null,
});
const publicRepositories = await octokit
.paginate("GET /users/:username/repos", { username: username })
.then((repositories) => toRepositoryList(repositories));
let allOwnedRepositories;
if (mirrorPrivateRepositories) {
allOwnedRepositories = await octokit
.paginate(
"GET /user/repos?visibility=public&affiliation=owner&visibility=private",
)
.then((repositories) => toRepositoryList(repositories));
}
let repositories = publicRepositories;
if (mirrorPrivateRepositories) {
repositories = filterDuplicates(
allOwnedRepositories.concat(publicRepositories),
);
}
if (!mirrorForks) {
repositories = repositories.filter((repository) => !repository.fork);
}
repositories = repositories.filter(
(repository) =>
include.some((f) => minimatch(repository.name, f)) &&
!exclude.some((f) => minimatch(repository.name, f)),
);
return repositories;
}
function toRepositoryList(repositories) {
return repositories.map((repository) => {
return {
name: repository.name,
url: repository.clone_url,
private: repository.private,
fork: repository.fork,
};
});
}
function filterDuplicates(array) {
const a = array.concat();
for (let i = 0; i < a.length; ++i) {
for (let j = i + 1; j < a.length; ++j) {
if (a[i].url === a[j].url) a.splice(j--, 1);
}
}
return a;
}
async function getGiteaUser(gitea) {
return request
.get(`${gitea.url}/api/v1/user`)
.set("Authorization", `token ${gitea.token}`)
.then((response) => {
return { id: response.body.id, name: response.body.username };
});
}
function isAlreadyMirroredOnGitea(repository, gitea, giteaUser) {
const requestUrl = `${gitea.url}/api/v1/repos/${giteaUser.name}/${repository}`;
return request
.get(requestUrl)
.set("Authorization", `token ${gitea.token}`)
.then(() => true)
.catch(() => false);
}
function mirrorOnGitea(repository, gitea, giteaUser, githubToken) {
request
.post(`${gitea.url}/api/v1/repos/migrate`)
.set("Authorization", `token ${gitea.token}`)
.send({
auth_token: githubToken || null,
clone_addr: repository.url,
mirror: true,
repo_name: repository.name,
uid: giteaUser.id,
private: repository.private,
})
.then(() => {
console.log("Did it!");
})
.catch((err) => {
console.log("Failed", err);
});
}
async function mirror(repository, gitea, giteaUser, githubToken, dryRun) {
if (await isAlreadyMirroredOnGitea(repository.name, gitea, giteaUser)) {
console.log(
"Repository is already mirrored; doing nothing: ",
repository.name,
);
return;
}
if (dryRun) {
console.log("DRY RUN: Would mirror repository to gitea: ", repository);
return;
}
console.log("Mirroring repository to gitea: ", repository.name);
await mirrorOnGitea(repository, gitea, giteaUser, githubToken);
}
import { Logger } from "./logger.js";
import getGithubRepositories from "./get-github-repositories.mjs";
async function main() {
let config;
@@ -135,45 +15,140 @@ async function main() {
process.exit(1);
}
console.log("Starting with the following configuration:");
console.log(` - GITHUB_USERNAME: ${config.github.username}`);
console.log(` - GITHUB_TOKEN: ${config.github.token ? "****" : ""}`);
console.log(
` - MIRROR_PRIVATE_REPOSITORIES: ${config.github.privateRepositories}`,
);
console.log(` - GITEA_URL: ${config.gitea.url}`);
console.log(` - GITEA_TOKEN: ${config.gitea.token ? "****" : ""}`);
console.log(` - SKIP_FORKS: ${config.github.skipForks}`);
console.log(` - DRY_RUN: ${config.dryRun}`);
console.log(` - INCLUDE: ${config.include}`);
console.log(` - EXCLUDE: ${config.exclude}`);
const logger = new Logger();
logger.showConfig(config);
const githubRepositories = await getGithubRepositories(
config.github.username,
config.github.token,
config.github.privateRepositories,
!config.github.skipForks,
config.include,
config.exclude,
// Create Gitea organization if specified
if (config.gitea.organization) {
await createGiteaOrganization(
{
url: config.gitea.url,
token: config.gitea.token,
},
config.gitea.organization,
config.gitea.visibility,
config.dryRun
);
}
// Create the starred repositories organization if mirror starred is enabled
if (config.github.mirrorStarred && config.gitea.starredReposOrg) {
await createGiteaOrganization(
{
url: config.gitea.url,
token: config.gitea.token,
},
config.gitea.starredReposOrg,
config.gitea.visibility,
config.dryRun
);
}
const octokit = new Octokit({
auth: config.github.token || null,
});
// Get user or organization repositories
const githubRepositories = await getGithubRepositories(octokit, {
username: config.github.username,
privateRepositories: config.github.privateRepositories,
skipForks: config.github.skipForks,
mirrorStarred: config.github.mirrorStarred,
mirrorOrganizations: config.github.mirrorOrganizations,
singleRepo: config.github.singleRepo,
includeOrgs: config.github.includeOrgs,
excludeOrgs: config.github.excludeOrgs,
preserveOrgStructure: config.github.preserveOrgStructure,
});
// Apply include/exclude filters
const filteredRepositories = githubRepositories.filter(
(repository) =>
config.include.some((f) => minimatch(repository.name, f)) &&
!config.exclude.some((f) => minimatch(repository.name, f)),
);
console.log(`Found ${githubRepositories.length} repositories on github`);
console.log(`Found ${filteredRepositories.length} repositories to mirror`);
const gitea = {
url: config.gitea.url,
token: config.gitea.token,
};
const giteaUser = await getGiteaUser(gitea);
// Get Gitea user information
const giteaUser = await getGiteaUser(gitea);
if (!giteaUser) {
console.error("Failed to get Gitea user. Exiting.");
process.exit(1);
}
// Create a map to store organization targets if preserving structure
const orgTargets = new Map();
if (config.github.preserveOrgStructure) {
// Get unique organization names from repositories
const uniqueOrgs = new Set(
filteredRepositories
.filter(repo => repo.organization)
.map(repo => repo.organization)
);
// Create or get each organization in Gitea
for (const orgName of uniqueOrgs) {
console.log(`Preparing Gitea organization for GitHub organization: ${orgName}`);
// Create the organization if it doesn't exist
await createGiteaOrganization(
gitea,
orgName,
config.gitea.visibility,
config.dryRun
);
// Get the organization details
const orgTarget = await getGiteaOrganization(gitea, orgName);
if (orgTarget) {
orgTargets.set(orgName, orgTarget);
} else {
console.error(`Failed to get or create Gitea organization: ${orgName}`);
}
}
}
// Mirror repositories
const queue = new PQueue({ concurrency: 4 });
await queue.addAll(
githubRepositories.map((repository) => {
filteredRepositories.map((repository) => {
return async () => {
// Determine the target (user or organization)
let giteaTarget;
if (config.github.preserveOrgStructure && repository.organization) {
// Use the organization as target
giteaTarget = orgTargets.get(repository.organization);
if (!giteaTarget) {
console.error(`No Gitea organization found for ${repository.organization}, using user instead`);
giteaTarget = config.gitea.organization
? await getGiteaOrganization(gitea, config.gitea.organization)
: giteaUser;
}
} else {
// Use the specified organization or user
giteaTarget = config.gitea.organization
? await getGiteaOrganization(gitea, config.gitea.organization)
: giteaUser;
}
await mirror(
repository,
gitea,
giteaUser,
{
url: config.gitea.url,
token: config.gitea.token,
skipStarredIssues: config.github.skipStarredIssues,
starredReposOrg: config.gitea.starredReposOrg
},
giteaTarget,
config.github.token,
config.github.mirrorIssues,
config.dryRun,
);
};
@@ -181,4 +156,313 @@ async function main() {
);
}
main();
// Get Gitea user information
async function getGiteaUser(gitea) {
try {
const response = await request
.get(`${gitea.url}/api/v1/user`)
.set("Authorization", `token ${gitea.token}`);
return {
id: response.body.id,
name: response.body.username,
type: "user"
};
} catch (error) {
console.error("Error fetching Gitea user:", error.message);
return null;
}
}
// Get Gitea organization information
async function getGiteaOrganization(gitea, orgName) {
try {
const response = await request
.get(`${gitea.url}/api/v1/orgs/${orgName}`)
.set("Authorization", `token ${gitea.token}`);
return {
id: response.body.id,
name: orgName,
type: "organization"
};
} catch (error) {
console.error(`Error fetching Gitea organization ${orgName}:`, error.message);
return null;
}
}
// Create a Gitea organization
async function createGiteaOrganization(gitea, orgName, visibility, dryRun) {
if (dryRun) {
console.log(`DRY RUN: Would create Gitea organization: ${orgName} (${visibility})`);
return true;
}
try {
// First check if organization already exists
try {
const existingOrg = await request
.get(`${gitea.url}/api/v1/orgs/${orgName}`)
.set("Authorization", `token ${gitea.token}`);
console.log(`Organization ${orgName} already exists`);
return true;
} catch (checkError) {
// Organization doesn't exist, continue to create it
}
const response = await request
.post(`${gitea.url}/api/v1/orgs`)
.set("Authorization", `token ${gitea.token}`)
.send({
username: orgName,
visibility: visibility || "public",
});
console.log(`Created organization: ${orgName}`);
return true;
} catch (error) {
// 422 error typically means the organization already exists
if (error.status === 422) {
console.log(`Organization ${orgName} already exists`);
return true;
}
console.error(`Error creating Gitea organization ${orgName}:`, error.message);
return false;
}
}
// Check if repository is already mirrored
async function isAlreadyMirroredOnGitea(repository, gitea, giteaTarget) {
const repoName = repository.name;
const ownerName = giteaTarget.name;
const requestUrl = `${gitea.url}/api/v1/repos/${ownerName}/${repoName}`;
try {
await request
.get(requestUrl)
.set("Authorization", `token ${gitea.token}`);
return true;
} catch (error) {
return false;
}
}
// Mirror repository to Gitea
async function mirrorOnGitea(repository, gitea, giteaTarget, githubToken) {
try {
const response = await request
.post(`${gitea.url}/api/v1/repos/migrate`)
.set("Authorization", `token ${gitea.token}`)
.send({
auth_token: githubToken || null,
clone_addr: repository.url,
mirror: true,
repo_name: repository.name,
uid: giteaTarget.id,
private: repository.private,
});
console.log(`Successfully mirrored: ${repository.name}`);
return response.body;
} catch (error) {
console.error(`Failed to mirror ${repository.name}:`, error.message);
throw error;
}
}
// Fetch issues for a repository
async function getGithubIssues(octokit, owner, repo) {
try {
const issues = await octokit.paginate("GET /repos/{owner}/{repo}/issues", {
owner,
repo,
state: "all",
per_page: 100,
});
return issues.map(issue => ({
title: issue.title,
body: issue.body || "",
state: issue.state,
labels: issue.labels.map(label => label.name),
closed: issue.state === "closed",
created_at: issue.created_at,
updated_at: issue.updated_at,
number: issue.number,
user: issue.user.login,
}));
} catch (error) {
console.error(`Error fetching issues for ${owner}/${repo}:`, error.message);
return [];
}
}
// Create an issue in a Gitea repository
async function createGiteaIssue(issue, repository, gitea, giteaTarget) {
try {
const response = await request
.post(`${gitea.url}/api/v1/repos/${giteaTarget.name}/${repository.name}/issues`)
.set("Authorization", `token ${gitea.token}`)
.send({
title: issue.title,
body: `*Originally created by @${issue.user} on ${new Date(issue.created_at).toLocaleDateString()}*\n\n${issue.body}`,
state: issue.state,
closed: issue.closed,
});
console.log(`Created issue #${response.body.number}: ${issue.title}`);
// Add labels if the issue has any
if (issue.labels && issue.labels.length > 0) {
await Promise.all(issue.labels.map(async (label) => {
try {
// First try to create the label if it doesn't exist
await request
.post(`${gitea.url}/api/v1/repos/${giteaTarget.name}/${repository.name}/labels`)
.set("Authorization", `token ${gitea.token}`)
.send({
name: label,
color: "#" + Math.floor(Math.random() * 16777215).toString(16), // Random color
})
.catch(() => {
// Label might already exist, which is fine
});
// Then add the label to the issue
await request
.post(`${gitea.url}/api/v1/repos/${giteaTarget.name}/${repository.name}/issues/${response.body.number}/labels`)
.set("Authorization", `token ${gitea.token}`)
.send({
labels: [label]
});
} catch (labelError) {
console.error(`Error adding label ${label} to issue:`, labelError.message);
}
}));
}
return response.body;
} catch (error) {
console.error(`Error creating issue "${issue.title}":`, error.message);
return null;
}
}
// Mirror issues for a repository
async function mirrorIssues(repository, gitea, giteaTarget, githubToken, dryRun) {
if (!repository.has_issues) {
console.log(`Repository ${repository.name} doesn't have issues enabled. Skipping issues mirroring.`);
return;
}
if (dryRun) {
console.log(`DRY RUN: Would mirror issues for repository: ${repository.name}`);
return;
}
try {
const octokit = new Octokit({ auth: githubToken });
const owner = repository.owner || repository.full_name.split('/')[0];
const issues = await getGithubIssues(octokit, owner, repository.name);
console.log(`Found ${issues.length} issues for ${repository.name}`);
// Create issues one by one to maintain order
for (const issue of issues) {
await createGiteaIssue(issue, repository, gitea, giteaTarget);
}
console.log(`Completed mirroring issues for ${repository.name}`);
} catch (error) {
console.error(`Error mirroring issues for ${repository.name}:`, error.message);
}
}
// Mirror a repository
async function mirror(repository, gitea, giteaTarget, githubToken, mirrorIssuesFlag, dryRun) {
// For starred repositories, use the starred repos organization if configured
if (repository.starred && gitea.starredReposOrg) {
// Get the starred repos organization
const starredOrg = await getGiteaOrganization(gitea, gitea.starredReposOrg);
if (starredOrg) {
console.log(`Using organization "${gitea.starredReposOrg}" for starred repository: ${repository.name}`);
giteaTarget = starredOrg;
} else {
console.log(`Could not find organization "${gitea.starredReposOrg}" for starred repositories, using default target`);
}
}
const isAlreadyMirrored = await isAlreadyMirroredOnGitea(repository, gitea, giteaTarget);
// Special handling for starred repositories
if (repository.starred) {
if (isAlreadyMirrored) {
console.log(`Repository ${repository.name} is already mirrored in ${giteaTarget.type} ${giteaTarget.name}; checking if it needs to be starred.`);
await starRepositoryInGitea(repository, gitea, giteaTarget, dryRun);
return;
} else if (dryRun) {
console.log(`DRY RUN: Would mirror and star repository to ${giteaTarget.type} ${giteaTarget.name}: ${repository.name} (starred)`);
return;
}
} else if (isAlreadyMirrored) {
console.log(`Repository ${repository.name} is already mirrored in ${giteaTarget.type} ${giteaTarget.name}; doing nothing.`);
return;
} else if (dryRun) {
console.log(`DRY RUN: Would mirror repository to ${giteaTarget.type} ${giteaTarget.name}: ${repository.name}`);
return;
}
console.log(`Mirroring repository to ${giteaTarget.type} ${giteaTarget.name}: ${repository.name}${repository.starred ? ' (will be starred)' : ''}`);
try {
await mirrorOnGitea(repository, gitea, giteaTarget, githubToken);
// Star the repository if it's marked as starred
if (repository.starred) {
await starRepositoryInGitea(repository, gitea, giteaTarget, dryRun);
}
// Mirror issues if requested and not in dry run mode
// Skip issues for starred repos if the skipStarredIssues option is enabled
const shouldMirrorIssues = mirrorIssuesFlag &&
!(repository.starred && gitea.skipStarredIssues);
if (shouldMirrorIssues && !dryRun) {
await mirrorIssues(repository, gitea, giteaTarget, githubToken, dryRun);
} else if (repository.starred && gitea.skipStarredIssues) {
console.log(`Skipping issues for starred repository: ${repository.name}`);
}
} catch (error) {
console.error(`Error during mirroring of ${repository.name}:`, error.message);
}
}
// Star a repository in Gitea
async function starRepositoryInGitea(repository, gitea, giteaTarget, dryRun) {
const ownerName = giteaTarget.name;
const repoName = repository.name;
if (dryRun) {
console.log(`DRY RUN: Would star repository in Gitea: ${ownerName}/${repoName}`);
return true;
}
try {
await request
.put(`${gitea.url}/api/v1/user/starred/${ownerName}/${repoName}`)
.set("Authorization", `token ${gitea.token}`);
console.log(`Successfully starred repository in Gitea: ${ownerName}/${repoName}`);
return true;
} catch (error) {
console.error(`Error starring repository ${ownerName}/${repoName}:`, error.message);
return false;
}
}
main().catch(error => {
console.error("Application error:", error);
process.exit(1);
});

27
src/logger.js Normal file
View File

@@ -0,0 +1,27 @@
import pino from "pino";
export class Logger {
constructor() {
this.pino = pino({
level: "trace",
redact: ["config.gitea.token", "config.github.token"],
timestamp: pino.stdTimeFunctions.isoTime,
formatters: {
level: this.prettyLevels(),
bindings: this.noPidOrHostname(),
},
});
}
prettyLevels() {
return (label) => ({ level: label });
}
noPidOrHostname() {
return () => {};
}
showConfig(config) {
this.pino.info({ config: config }, "applied configuration");
}
}