Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ jobs:
- `source_token` (optional): Access token for private source repos (required for private HTTPS repos without embedded credentials). When provided with `destination_token`, enables different tokens for source and destination repos.
- `destination_token` (optional): Access token specifically for destination repo. When provided, `source_token` is required. Enables using different credentials for source and destination repositories.
- `sync_all_branches` (optional): `true` to sync all branches from source repo
- `use_lightweight_sync` (optional): `true` to use lightweight sync mode optimized for large repositories (faster, uses force push without incremental logic). Default: `false`
- `use_main_as_fallback` (optional): `true` (default) to fallback to `main` or `master` if specified branch not found, `false` for strict branch matching

**Authentication (provide one of the following):**
Expand All @@ -206,6 +207,59 @@ jobs:

> **Note**: For HTTPS URLs, provide either a `github_token` OR all three GitHub App parameters OR use `source_token`/`destination_token` pair for separate credentials. SSH URLs don't require authentication if SSH keys are configured.

### Lightweight Sync Mode

For repositories with large histories or when you need faster synchronization, use **lightweight sync mode** by setting `use_lightweight_sync: "true"`. This mode optimizes performance for large repositories by:

- **Skipping incremental logic**: Uses direct force push instead of analyzing commits
- **Faster performance**: Ideal for repositories with extensive history or large file sizes
- **Optimized for clones**: Better suited for initial syncs or full repository mirrors
- **No history analysis**: Doesn't check if source is ahead or behind, simply force pushes all changes

#### When to Use Lightweight Sync

- **Large repositories** (>1GB or deep history): Significantly faster than standard sync
- **Initial mirror setup**: Perfect for cloning entire repositories
- **Periodic backups**: When you just need a quick mirror without incremental checks
- **Network-constrained environments**: Reduces unnecessary git operations

#### Standard vs Lightweight Sync

| Aspect | Standard Sync | Lightweight Sync |
|--------|---------------|-----------------|
| **Performance** | Analyzes commits to sync only new changes | Direct force push, no analysis |
| **Best For** | Regular incremental updates | Large repos or initial setup |
| **History Check** | Compares commit trees | N/A (force pushes all refs) |
| **Repository Size** | Good for any size | Optimized for large repos |
| **Speed** | Slower on large repos | Significantly faster |

#### Example: Lightweight Sync for Large Repository

```yaml
- uses: renan-alm/github-repo-sync@v2
with:
source_repo: "https://github.com/org/huge-repo.git"
source_branch: "main"
destination_repo: "https://github.com/org/mirror-repo.git"
destination_branch: "main"
use_lightweight_sync: "true" # Enable lightweight mode
sync_tags: "true"
github_token: ${{ secrets.PAT }}
```

#### Example: Lightweight Sync with All Branches

```yaml
- uses: renan-alm/github-repo-sync@v2
with:
source_repo: "https://github.com/org/large-repo.git"
destination_repo: "https://github.com/org/destination-repo.git"
use_lightweight_sync: "true"
sync_all_branches: "true"
sync_tags: "true"
github_token: ${{ secrets.PAT }}
```

### Workflow Considerations

#### Branch Fallback Behavior
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ inputs:
ssh_strict_host_key_checking:
description: Enable strict SSH host key checking (default true)
required: false
use_lightweight_sync:
description: Use lightweight sync for large repositories (faster, uses temporary remote instead of clone)
required: false
runs:
using: "node20"
main: "dist/index.js"
230 changes: 228 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42756,6 +42756,219 @@ async function setupSSHAuthentication(sshConfig, hostsToValidate = ["github.com"
}
}

;// CONCATENATED MODULE: ./src/lightweight-sync.js



/**
* Lightweight sync for large repositories
* Uses the source remote that's already configured
* Much faster for large repositories since it doesn't need full git history.
* Approach: Fetch from source remote and force push to destination
*/
async function syncBranchesLightweight(
sourceBranch,
destinationBranch,
syncAllBranches,
) {
lib_core.info("=== Starting Lightweight Sync (Large Repository Mode) ===");
lib_core.info(
`Using lightweight approach - optimized for large repositories`,
);

if (syncAllBranches) {
lib_core.info("=== Syncing All Branches (Lightweight) ===");

// Get all source branches
let sourceBranchesOutput = "";
await exec.exec("git", ["branch", "-r", "--list", "source/*"], {
listeners: {
stdout: (data) => {
sourceBranchesOutput += data.toString();
},
},
});

const sourceBranches = sourceBranchesOutput
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !line.includes("HEAD"))
.map((line) => line.replace("source/", ""));

lib_core.info(`Found ${sourceBranches.length} source branches`);

// Create branch mapping: source_branch → destination_branch, others keep same name
const branchMapping = {};
for (const branch of sourceBranches) {
branchMapping[branch] =
branch === sourceBranch ? destinationBranch : branch;
}

lib_core.info(`Branch mapping: ${JSON.stringify(branchMapping)}`);

// Push all branches
for (const sourceBranchName of sourceBranches) {
const destBranchName = branchMapping[sourceBranchName];
lib_core.info(`Pushing: ${sourceBranchName} → ${destBranchName}`);

try {
await exec.exec("git", [
"push",
"origin",
`refs/remotes/source/${sourceBranchName}:refs/heads/${destBranchName}`,
"--force",
]);
lib_core.info(`✓ Branch pushed: ${sourceBranchName} → ${destBranchName}`);
} catch (error) {
lib_core.error(
`Failed to push ${sourceBranchName} → ${destBranchName}: ${error.message}`,
);
throw error;
}
}
} else {
lib_core.info(`=== Syncing Single Branch (Lightweight) ===`);
lib_core.info(`Pushing: ${sourceBranch} → ${destinationBranch}`);

try {
await exec.exec("git", [
"push",
"origin",
`refs/remotes/source/${sourceBranch}:refs/heads/${destinationBranch}`,
"--force",
]);
lib_core.info(
`✓ Branch pushed: ${sourceBranch} → ${destinationBranch}`,
);
} catch (error) {
lib_core.error(
`Failed to push ${sourceBranch} → ${destinationBranch}: ${error.message}`,
);
throw error;
}
}
}

/**
* Lightweight tag sync for large repositories
* Uses direct git tag commands for efficiency
* Fetches tags from source and pushes to destination with --force
*/
async function syncTagsLightweight(syncTags) {
if (syncTags === "true") {
lib_core.info("=== Syncing All Tags (Lightweight) ===");

try {
// Delete all local tags to ensure clean state (matching inspiration.sh behavior)
lib_core.info("Cleaning up local tags...");
let tagsOutput = "";
await exec.exec("git", ["tag", "-l"], {
listeners: {
stdout: (data) => {
tagsOutput += data.toString();
},
},
});

if (tagsOutput.trim()) {
const tags = tagsOutput.split(/\r?\n/).filter((tag) => tag.trim());
for (const tag of tags) {
if (tag) {
await exec.exec("git", ["tag", "-d", tag], {
silent: true,
});
}
}
lib_core.info(`✓ Deleted ${tags.length} local tags`);
}

// Fetch all tags from source
lib_core.info("Fetching tags from source...");
await exec.exec("git", ["fetch", "source", "--tags", "--quiet"]);

// Push all tags to destination
lib_core.info("Pushing all tags to destination...");
await exec.exec("git", ["push", "origin", "--tags", "--force"]);
lib_core.info("✓ All tags synced");
} catch (error) {
lib_core.error(`Failed to sync tags: ${error.message}`);
throw error;
}
} else if (syncTags) {
lib_core.info("=== Syncing Tags Matching Pattern (Lightweight) ===");
lib_core.info(`Pattern: ${syncTags}`);

try {
// Delete all local tags to ensure clean state (matching inspiration.sh behavior)
lib_core.info("Cleaning up local tags...");
let tagsOutput = "";
await exec.exec("git", ["tag", "-l"], {
listeners: {
stdout: (data) => {
tagsOutput += data.toString();
},
},
});

if (tagsOutput.trim()) {
const tags = tagsOutput.split(/\r?\n/).filter((tag) => tag.trim());
for (const tag of tags) {
if (tag) {
await exec.exec("git", ["tag", "-d", tag], {
silent: true,
});
}
}
lib_core.info(`✓ Deleted ${tags.length} local tags`);
}

// Fetch all tags
lib_core.info("Fetching tags from source...");
await exec.exec("git", ["fetch", "source", "--tags", "--quiet"]);

// Get all tags
let allTagsOutput = "";
await exec.exec("git", ["tag"], {
listeners: {
stdout: (data) => {
allTagsOutput += data.toString();
},
},
});

const allTags = allTagsOutput
.split(/\r?\n/)
.filter((tag) => tag.trim());
const matchingTags = allTags.filter((tag) => tag.match(syncTags));

lib_core.info(`Found ${matchingTags.length} matching tags`);

// Push matching tags
for (const tag of matchingTags) {
if (tag) {
try {
lib_core.info(`Pushing tag: ${tag}`);
await exec.exec("git", [
"push",
"origin",
`refs/tags/${tag}:refs/tags/${tag}`,
"--force",
]);
lib_core.info(`✓ Tag pushed: ${tag}`);
} catch (error) {
lib_core.warning(`Failed to push tag ${tag}: ${error.message}`);
}
}
}
} catch (error) {
lib_core.error(`Failed to sync tags: ${error.message}`);
throw error;
}
} else {
lib_core.info("Tag syncing disabled");
}
}

;// CONCATENATED MODULE: ./src/index.js


Expand All @@ -42764,6 +42977,7 @@ async function setupSSHAuthentication(sshConfig, hostsToValidate = ["github.com"




async function getAppInstallationToken(appId, privateKey, installationId) {
const auth = (0,dist_node.createAppAuth)({
appId,
Expand All @@ -42789,6 +43003,7 @@ function readInputs() {
githubAppId: lib_core.getInput("github_app_id"),
githubAppPrivateKey: lib_core.getInput("github_app_private_key"),
githubAppInstallationId: lib_core.getInput("github_app_installation_id"),
useLightweightSync: lib_core.getInput("use_lightweight_sync") === "true",
// SSH inputs
sshKey: lib_core.getInput("ssh_key"),
sshKeyPath: lib_core.getInput("ssh_key_path"),
Expand Down Expand Up @@ -43539,8 +43754,19 @@ async function run() {

await setupSourceRemote(srcUrl);

// Use Gerrit-specific or standard sync based on detection
if (isDestinationGerrit) {
// Choose sync strategy
if (inputs.useLightweightSync) {
lib_core.info("Using lightweight sync mode (optimized for large repositories)");
// Lightweight sync doesn't cd into a repo directory
// It works on the current directory with temporary remotes
await syncBranchesLightweight(
inputs.sourceBranch,
inputs.destinationBranch,
inputs.syncAllBranches,
);
await syncTagsLightweight(inputs.syncTags);
} else if (isDestinationGerrit) {
// Use Gerrit-specific or standard sync based on detection
logGerritInfo(inputs.sourceRepo, inputs.destinationRepo);
await syncBranchesGerrit(
inputs.sourceBranch,
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import {
detectAuthenticationMethods,
setupSSHAuthentication,
} from "./ssh-auth.js";
import {
syncBranchesLightweight,
syncTagsLightweight,
} from "./lightweight-sync.js";

async function getAppInstallationToken(appId, privateKey, installationId) {
const auth = createAppAuth({
Expand All @@ -43,6 +47,7 @@ function readInputs() {
githubAppId: core.getInput("github_app_id"),
githubAppPrivateKey: core.getInput("github_app_private_key"),
githubAppInstallationId: core.getInput("github_app_installation_id"),
useLightweightSync: core.getInput("use_lightweight_sync") === "true",
// SSH inputs
sshKey: core.getInput("ssh_key"),
sshKeyPath: core.getInput("ssh_key_path"),
Expand Down Expand Up @@ -793,8 +798,19 @@ async function run() {

await setupSourceRemote(srcUrl);

// Use Gerrit-specific or standard sync based on detection
if (isDestinationGerrit) {
// Choose sync strategy
if (inputs.useLightweightSync) {
core.info("Using lightweight sync mode (optimized for large repositories)");
// Lightweight sync doesn't cd into a repo directory
// It works on the current directory with temporary remotes
await syncBranchesLightweight(
inputs.sourceBranch,
inputs.destinationBranch,
inputs.syncAllBranches,
);
await syncTagsLightweight(inputs.syncTags);
} else if (isDestinationGerrit) {
// Use Gerrit-specific or standard sync based on detection
logGerritInfo(inputs.sourceRepo, inputs.destinationRepo);
await syncBranchesGerrit(
inputs.sourceBranch,
Expand Down
Loading
Loading