Skip to content
Draft
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
191 changes: 191 additions & 0 deletions .github/actions/sync-fork-checkpoints/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: 'Sync Fork Checkpoints'
description: 'Import Entire session checkpoint data from a fork after a PR is merged'

inputs:
token:
description: 'GitHub token with contents:write permission. Defaults to GITHUB_TOKEN.'
required: false
default: ${{ github.token }}

outputs:
imported_count:
description: 'Number of checkpoint commits cherry-picked'
value: ${{ steps.sync.outputs.imported_count }}
synced:
description: 'Whether any checkpoints were synced (true/false)'
value: ${{ steps.sync.outputs.synced }}

runs:
using: 'composite'
steps:
- name: Sync fork checkpoints
id: sync
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
REPO: ${{ github.repository }}
FORK_URL: ${{ github.event.pull_request.head.repo.clone_url }}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script accepts FORK_URL from untrusted input (github.event.pull_request.head.repo.clone_url). While there's validation that IS_FORK is true, malicious fork URLs could potentially be crafted to exploit git operations. Consider adding validation to ensure the FORK_URL has an expected format (e.g., matches https://github.com/* pattern) before using it in git fetch commands to prevent potential security issues.

Copilot uses AI. Check for mistakes.
FORK_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

BRANCH="entire/checkpoints/v1"
ORIGIN_URL="https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"

echo "synced=false" >> "$GITHUB_OUTPUT"
echo "imported_count=0" >> "$GITHUB_OUTPUT"

# --- Guard: only run for fork PRs ---
if [ "$IS_FORK" != "true" ]; then
echo "PR is not from a fork. Skipping."
exit 0
fi

# --- Set up a minimal repo (no full checkout, only checkpoints branch) ---
WORKDIR=$(mktemp -d)
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WORKDIR temporary directory is not cleaned up on script exit. While GitHub Actions will clean up after the job completes, it's good practice to add cleanup on exit. Consider adding a trap to clean up the temporary directory: trap "rm -rf '$WORKDIR'" EXIT

Suggested change
WORKDIR=$(mktemp -d)
WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT

Copilot uses AI. Check for mistakes.
cd "$WORKDIR"
git init
git remote add origin "$ORIGIN_URL"

# Fetch only the merge range commits (for reading trailers)
git fetch origin "$MERGE_SHA" --depth=100 2>/dev/null || true
git fetch origin "$BASE_SHA" --depth=100 2>/dev/null || true
Comment on lines +56 to +57
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shallow fetch depth of 100 commits may not be sufficient for long-lived PRs or repositories with many commits. If the BASE_SHA is not within the last 100 commits, git log will fail silently (due to || true), and no checkpoint trailers will be found. Consider using a deeper fetch depth or fetching without depth limitation for the merge range. Alternatively, document this limitation in the action description.

Copilot uses AI. Check for mistakes.

# --- Step 1: Find checkpoint IDs in merged commits ---
echo "Looking for Entire-Checkpoint trailers in ${BASE_SHA:0:7}..${MERGE_SHA:0:7}"

CHECKPOINT_IDS=$(git log --format='%(trailers:key=Entire-Checkpoint,valueonly)' \
"${BASE_SHA}..${MERGE_SHA}" 2>/dev/null \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| grep -v '^$' | sort -u || true)

# Fallback: check original PR commits (handles squash merges that drop trailers)
if [ -z "$CHECKPOINT_IDS" ]; then
echo "No trailers in merge range. Checking original PR commits..."
git fetch "$FORK_URL" "$HEAD_SHA" --depth=50 2>/dev/null || true
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic for squash merges fetches from the fork using HEAD_SHA, but the depth of 50 commits may not be sufficient for long-lived PRs. If the BASE_SHA is not within the last 50 commits from HEAD_SHA, the git log range will be incorrect. Consider either using a deeper fetch or removing the depth limitation for this fallback case to ensure all commits in the PR are examined.

Suggested change
git fetch "$FORK_URL" "$HEAD_SHA" --depth=50 2>/dev/null || true
git fetch "$FORK_URL" "$HEAD_SHA" 2>/dev/null || true

Copilot uses AI. Check for mistakes.
CHECKPOINT_IDS=$(git log --format='%(trailers:key=Entire-Checkpoint,valueonly)' \
"${BASE_SHA}..${HEAD_SHA}" 2>/dev/null \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| grep -v '^$' | sort -u || true)
fi

if [ -z "$CHECKPOINT_IDS" ]; then
echo "No Entire-Checkpoint trailers found. Nothing to sync."
exit 0
fi

echo "Found checkpoint IDs:"
echo "$CHECKPOINT_IDS"
echo ""

# --- Step 2: Fetch fork's checkpoints branch ---
echo "Fetching $BRANCH from fork ($FORK_FULL_NAME)..."
if ! git fetch "$FORK_URL" "$BRANCH" 2>/dev/null; then
echo "Fork has no $BRANCH branch. Nothing to sync."
exit 0
fi
FORK_REF=$(git rev-parse FETCH_HEAD)
echo "Fork's $BRANCH is at ${FORK_REF:0:7}"

# --- Step 3: Validate checkpoint IDs ---
VALID_IDS=()
for ID in $CHECKPOINT_IDS; do
if echo "$ID" | grep -qE '^[0-9a-f]{12}$'; then
VALID_IDS+=("$ID")
else
echo " Skipping invalid checkpoint ID: $ID"
fi
done

if [ ${#VALID_IDS[@]} -eq 0 ]; then
echo "No valid checkpoint IDs found. Nothing to sync."
exit 0
fi

# --- Step 4: Find matching commits on fork's checkpoints branch ---
GREP_PATTERN=$(printf '%s\n' "${VALID_IDS[@]}" | paste -sd '|' -)

# Fetch upstream's checkpoints branch to determine fork-only commits
git fetch origin "$BRANCH" 2>/dev/null || true

if git rev-parse --verify "refs/remotes/origin/$BRANCH" >/dev/null 2>&1; then
FORK_RANGE="origin/${BRANCH}..${FORK_REF}"
else
FORK_RANGE="${FORK_REF}"
fi

# Find commits referencing our checkpoint IDs, in chronological order
COMMITS=$(git log --reverse --format='%H' --extended-regexp \
--grep="(${GREP_PATTERN})" "$FORK_RANGE" 2>/dev/null || true)

if [ -z "$COMMITS" ]; then
echo "No matching commits found on fork's $BRANCH. Nothing to sync."
exit 0
fi

COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ')
echo "Found $COMMIT_COUNT commit(s) to cherry-pick"

# --- Step 5: Set up git identity ---
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
Comment on lines +135 to +136
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The email address uses the generic github-actions bot email. While this is functional, consider using a more specific identity that indicates this is an automated checkpoint sync operation. This would make it clearer in the git history who created these commits. For example: "github-actions[bot]" with a comment in the commit message indicating it's from the sync-fork-checkpoints action.

Suggested change
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "sync-fork-checkpoints[bot]"
git config user.email "sync-fork-checkpoints[bot]@users.noreply.github.com"

Copilot uses AI. Check for mistakes.

# --- Step 6: Check out or create the local checkpoints branch ---
if git rev-parse --verify "refs/remotes/origin/$BRANCH" >/dev/null 2>&1; then
echo "Checking out existing $BRANCH..."
git checkout -B "$BRANCH" "origin/$BRANCH"
else
echo "Creating new orphan branch $BRANCH..."
git checkout --orphan "$BRANCH"
git rm -rf . 2>/dev/null || true
git commit --allow-empty -m "Initialize $BRANCH"
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When creating a new orphan branch (lines 143-146), the script creates an empty initial commit. However, if this is the first time the checkpoints branch is being created and there are valid checkpoints to import from a fork, the resulting branch history will start with an empty commit followed by the cherry-picked commits. Consider checking if there are checkpoints to import before creating the initial empty commit, or document that the empty commit serves as a branch initialization marker.

Suggested change
git commit --allow-empty -m "Initialize $BRANCH"

Copilot uses AI. Check for mistakes.
fi

# --- Step 7: Cherry-pick each commit (preserves messages, trailers, authorship) ---
IMPORTED=0
while IFS= read -r COMMIT; do
[ -z "$COMMIT" ] && continue
SUBJECT=$(git log -1 --format='%s' "$COMMIT")
echo " Cherry-picking: $SUBJECT (${COMMIT:0:7})"
if git cherry-pick "$COMMIT" --no-edit; then
IMPORTED=$((IMPORTED + 1))
else
git cherry-pick --abort 2>/dev/null || true
echo " Warning: failed to cherry-pick ${COMMIT:0:7}, skipping"
fi
Comment on lines +155 to +160
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cherry-pick logic silently skips commits that fail to apply (lines 157-159). While this prevents the entire sync from failing, it means some checkpoint data might be lost without clear indication to users. Consider logging which commits were skipped to the action summary or output, so that users can investigate failed cherry-picks. You could collect skipped commit hashes in an array and display them at the end.

Copilot uses AI. Check for mistakes.
done <<< "$COMMITS"

if [ "$IMPORTED" -eq 0 ]; then
echo "No commits were successfully cherry-picked. Nothing to sync."
exit 0
fi

# --- Step 8: Push only the checkpoints branch (with retry for concurrent merges) ---
MAX_RETRIES=3
for ATTEMPT in $(seq 1 $MAX_RETRIES); do
if git push origin "$BRANCH"; then
echo ""
echo "Successfully cherry-picked ${IMPORTED} commit(s) from PR #${PR_NUMBER}"
echo "synced=true" >> "$GITHUB_OUTPUT"
echo "imported_count=${IMPORTED}" >> "$GITHUB_OUTPUT"
exit 0
fi

if [ "$ATTEMPT" -lt "$MAX_RETRIES" ]; then
echo "Push failed (attempt $ATTEMPT/$MAX_RETRIES). Rebasing on remote..."
git fetch origin "$BRANCH"
git rebase "origin/$BRANCH" || {
echo "Rebase failed, trying merge..."
git rebase --abort 2>/dev/null || true
git merge "origin/$BRANCH" --no-edit
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry mechanism uses merge as a fallback after rebase fails, but it doesn't check if the merge itself was successful. If the merge fails, the script will continue to the next retry attempt without cleaning up the failed merge state. Add error handling after the merge command to ensure it succeeded: git merge "origin/$BRANCH" --no-edit || { echo "Merge failed"; git merge --abort 2>/dev/null || true; }

Suggested change
git merge "origin/$BRANCH" --no-edit
git merge "origin/$BRANCH" --no-edit || { echo "Merge failed"; git merge --abort 2>/dev/null || true; }

Copilot uses AI. Check for mistakes.
}
fi
done

echo "::warning::Failed to push checkpoints after $MAX_RETRIES attempts"
exit 1
Comment on lines +190 to +191
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script initializes outputs at the start (lines 40-41) and updates them on successful sync (lines 174-175), but there's a logic issue: when the script exits early due to errors or no checkpoints found, the outputs remain at their default values (synced=false, imported_count=0). However, line 190 uses exit 1 for failure but doesn't update the outputs. This means consumers of this action can't distinguish between "no checkpoints to sync" (successful exit 0) and "push failed after retries" (exit 1) by checking the outputs alone. Consider setting an error output or ensuring the exit status is the primary indicator of failure.

Copilot uses AI. Check for mistakes.
32 changes: 32 additions & 0 deletions .github/workflows/sync-fork-checkpoints.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Sync Entire session checkpoints from fork PRs.
#
# When a PR from a fork is merged, this workflow imports the checkpoint data
# (session transcripts, prompts, context) from the fork's entire/checkpoints/v1
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment mentions "entire/sessions/v1" but the actual branch name used throughout the code is "entire/checkpoints/v1". This is inconsistent with the PR description and could cause confusion. The correct branch name should be "entire/checkpoints/v1" as verified in the codebase.

Copilot uses AI. Check for mistakes.
# branch into the upstream repo's entire/checkpoints/v1 branch.
#
# This enables the full Entire session history to be preserved even when
# contributors work from forks without push access to upstream.
#
# How it works:
# 1. Finds Entire-Checkpoint trailers in the merged commits
# 2. Fetches the fork's entire/checkpoints/v1 branch
# 3. Selectively imports only the referenced checkpoint directories
# 4. Pushes the updated checkpoints branch to upstream

name: Sync Fork Checkpoints

on:
pull_request_target:
types: [closed]
Comment on lines +19 to +20
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using pull_request_target is necessary for write access but is a security consideration. The workflow correctly guards against running on non-fork PRs and only runs after merge, which limits the attack surface. However, be aware that this gives the workflow access to repository secrets and write permissions. The current implementation is safe because it only operates on git commit metadata (trailers) and the checkpoints branch, but any future modifications should be carefully reviewed for security implications.

Copilot uses AI. Check for mistakes.

permissions:
contents: write

jobs:
sync-checkpoints:
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.head.repo.fork == true
runs-on: ubuntu-latest
steps:
- uses: entireio/cli/.github/actions/sync-fork-checkpoints@main
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The action reference should use a relative path for local actions in the same repository. Change this to use a relative path starting with ./ instead of the full repository path. Using the full repository path entireio/cli/.github/actions/...@main will cause GitHub Actions to fetch the action from the main branch instead of using the version in the current commit, which could lead to unexpected behavior during development and testing.

Suggested change
- uses: entireio/cli/.github/actions/sync-fork-checkpoints@main
- uses: ./.github/actions/sync-fork-checkpoints

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +32
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow is missing a required checkout step. Since the action being called is a local composite action (stored in .github/actions/sync-fork-checkpoints/), GitHub Actions needs to checkout the repository first to access the action definition. Add - uses: actions/checkout@v6 as the first step before calling the action. Without this, the workflow will fail because the action code won't be available.

Copilot uses AI. Check for mistakes.