diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be6a716..212a6bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,13 +3,8 @@ on: pull_request: workflow_dispatch: jobs: - CI: - strategy: - matrix: - runs-on: [ubuntu-latest, windows-latest, macos-latest] - goos: [linux, windows, darwin] - goarch: [amd64, arm64] - runs-on: ${{ matrix.runs-on }} + lint: + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 @@ -21,8 +16,47 @@ jobs: uses: golangci/golangci-lint-action@v4 with: version: v1.54.2 + unit-test: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.21 - name: Unit Tests run: ./script/test + e2e-test: + needs: unit-test + runs-on: ubuntu-latest + strategy: + matrix: + platform: [linux, windows] + arch: [arm, arm64] - - name: Run End-to-End Tests for ${{matrix.goos}}/${{matrix.goarch}} - run: ./script/test-build-ci ${{matrix.goos}} ${{matrix.goarch}} + steps: + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.21 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Binary for ${{ matrix.platform }}-${{ matrix.arch }} + run: | + if [[ "${{ matrix.platform }}" == "linux" ]]; then + docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + fi + + GOOS=${{ matrix.platform }} GOARCH=${{ matrix.arch }} go build -o bin/actions-sync-${{ matrix.platform }}-${{ matrix.arch }} main.go + + - name: Run End-to-End Tests for ${{ matrix.platform }}-${{ matrix.arch }} + run: ./script/test-build-ci bin/actions-sync-${{ runner.platform }}-${{ runner.arch }} diff --git a/script/build-test-ci b/script/build-test-ci deleted file mode 100755 index 817275d..0000000 --- a/script/build-test-ci +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -if [ -z "$1" ]; then - echo "No GOOS specified as first arg, using the current system GOOS" - GOOS_BUILD=$(go env GOOS) -else - GOOS_BUILD=$1 -fi - -if [ -z "$2" ]; then - echo "No GOARCH specified as second arg, using the current system GOOS" - GOARCH_BUILD=$(go env GOARCH) -else - GOARCH_BUILD=$1 -fi - -echo "Building for $GOOS_BUILD $GOARCH_BUILD" -# build the exutable and include GOOS and GOARCH in the name - -GOOS=$GOOS_BUILD GOARCH=$GOARCH_BUILD go build -o bin/actions-sync-"$GOOS_BUILD"-"$GOARCH_BUILD" main.go diff --git a/script/test-ci b/script/test-ci new file mode 100755 index 0000000..847ff9f --- /dev/null +++ b/script/test-ci @@ -0,0 +1,426 @@ +#!/usr/bin/env bash + +#script/build + +# if the first argument is missing then print usage and exit +if [ -z "$1" ]; then + echo "Usage: $1 " + exit 1 +fi + +ACTIONS_SYNC_BINARY=$1 + +OUTPUT=$(mktemp) +RESULT=-1 + +SRC_GIT_DAEMON_PORT=9419 +SRC_GIT_DAEMON_OUTPUT=$(mktemp) +SRC_GIT_DAEMON_PID=-1 + +DEST_GIT_DAEMON_PORT=9420 +DEST_GIT_DAEMON_OUTPUT=$(mktemp) +DEST_GIT_DAEMON_PID=-1 + +DEST_API_PORT=8081 +DEST_API_OUTPUT=$(mktemp) +DEST_API_PID=-1 +trap "after_suite" EXIT +trap "after_suite" SIGINT + +function test_version() { + version "version shouldn't require any flags" + + echo "all version tests passed" +} + +function test_pull() { + # Pull new repo + setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_cache + pull --repo-name "org/repo" "pulling new repo" + assert_cache_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "pulling new repo org/repo" + + echo "all pull tests passed successfully" +} + +function test_push() { + # Push with a new change to main + setup_cache "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + push "pushing new commit to main" + assert_dest_sha "org/repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo:heads/main to new commit" + + # Push a non-linear change + setup_cache "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_dest "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + + push "pushing to an old commit to main" + assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "updating heads/main to an old commit" + + # Push with no changes to main + setup_cache "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + push "pushing no changes to main" + assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "leaving org/repo:heads/main at existing commit" + + # Push multiple branches + setup_cache "org/repo:heads/change:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \ + "org/repo:heads/nochange:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_dest "org/repo:heads/change:a5984bb887dd2fcdc2892cd906d6f004844d1142" \ + "org/repo:heads/nochange:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + push "pushing multiple branches" + assert_dest_sha "org/repo" "heads/change" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo:heads/change to new commit" + assert_dest_sha "org/repo" "heads/nochange" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "leaving org/repo:heads/nochange at existing commit" + + # Pushing multiple branches and tags + setup_cache "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \ + "org/repo:tags/v1:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \ + "org/repo:tags/v1:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + push "pushing multiple branches and tags" + assert_dest_sha "org/repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo:heads/main to new commit" + assert_dest_sha "org/repo" "tags/v1" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo:tags/v1 to new commit" + + # Pushing multiple repositories + setup_cache "org1/repo1:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \ + "org1/repo2:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \ + "org1/repo2:tags/v1:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \ + "org2/repo1:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org1/repo1:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \ + "org1/repo2:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \ + "org1/repo2:tags/v1:a5984bb887dd2fcdc2892cd906d6f004844d1142" \ + "org2/repo1:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + push "pushing multiple repositories" + assert_dest_sha "org1/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org1/repo1:heads/main to new commit" + assert_dest_sha "org1/repo2" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org1/repo2:heads/main to new commit" + assert_dest_sha "org1/repo2" "tags/v1" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org1/repo2:tags/v1 to new commit" + assert_dest_sha "org2/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org2/repo1:tags/v1 to new commit" + + # Honor --repo-name flag, ignore other cache entries + setup_cache "org/repo1:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \ + "org/repo2:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org/repo1:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \ + "org/repo2:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + push2args --repo-name "org/repo1" "pushing only one of the repos in the cache" + assert_dest_sha "org/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo1 passed in repo flag" + assert_dest_sha "org/repo2" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "org/repo2 not updated despite cache" + + # Push to pre-existing org + setup_cache "org-already-exists/new-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org-already-exists/new-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + push "pushing to existing org" + assert_dest_sha "org-already-exists/new-repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org-already-exists/new-repo" + + # Push to pre-existing repo + setup_cache "org-already-exists/repo-already-exists:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org-already-exists/repo-already-exists:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + push "pushing to existing repo" + assert_dest_sha "org-already-exists/repo-already-exists" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org-already-exists/repo-already-exists" + + # Push to repo in user's account + setup_cache "monalisa/new-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "monalisa/new-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + push "pushing to authenticated user's account" + assert_dest_sha "monalisa/new-repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating monalisa/new-repo" + + # Push to GHAE with impersonation + setup_cache "org-already-exists/ghae-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org-already-exists/ghae-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + push_impersonation "ghae-admin" "pushing to GHAE repo" + + # Push to GHES with impersonation + setup_cache "org-already-exists/ghes-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org-already-exists/ghes-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + push_impersonation "ghes-admin" "pushing to GHES repo" + + echo "all push tests passed successfully" +} + +function test_sync() { + # Sync no change without cached + setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_cache + setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + sync --repo-name "org/repo" "syncing a new commit in an uncached repo" + assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "syncing no changes without cache" + + # Sync a new commit without cache + setup_src "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_cache + setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + sync --repo-name "org/repo" "syncing a new commit in an uncached repo" + assert_dest_sha "org/repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "syncing a new commit without cache" + + # Sync no change with uncached + setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_cache "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + sync --repo-name "org/repo" "syncing a new commit in an uncached repo" + assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "syncing no changes with cache" + + # Sync a new commit with cache + setup_src "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_cache "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + sync --repo-name "org/repo" "syncing a new commit in an uncached repo" + assert_dest_sha "org/repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "syncing a new commit with cache" + + # Sync a non-linear change without cache + setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_cache + setup_dest "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + + sync --repo-name "org/repo" "syncing an old commit in an uncached repo" + assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "syncing heads/main to an old commit without cache" + + # Sync a non-linear change with cache + setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + setup_cache "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + + sync --repo-name "org/repo" "syncing an old commit in an cached repo" + assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "syncing heads/main to an old commit with cache" + + # Sync to a different repo + setup_src "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_cache + setup_dest "org2/repo2:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + sync --repo-name "org/repo:org2/repo2" "syncing org/repo aliased to org2/repo2" + assert_dest_sha "org2/repo2" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "syncing org/repo aliased to org2/repo2" + + # Honor --repo-name flag, ignore other cache entries + setup_cache "org/repo1:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \ + "org/repo2:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" + setup_dest "org/repo1:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \ + "org/repo2:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" + + sync --repo-name "org/repo1" "syncing only one of the repos in the cache" + assert_dest_sha "org/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo1 passed in repo flag" + assert_dest_sha "org/repo2" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "org/repo2 not updated despite cache" + + echo "all sync tests passed successfully" +} + +function before_suite() { + set -e + $( + rm -rf test/tmp + mkdir -p test/tmp/dest + mkdir -p test/tmp/src + ) >/dev/null + + git daemon \ + --reuseaddr \ + --verbose \ + --export-all \ + --enable=receive-pack \ + --port="$SRC_GIT_DAEMON_PORT" \ + --base-path=test/tmp/src &>$SRC_GIT_DAEMON_OUTPUT & + SRC_GIT_DAEMON_PID=$! + + git daemon \ + --reuseaddr \ + --verbose \ + --export-all \ + --enable=receive-pack \ + --port="$DEST_GIT_DAEMON_PORT" \ + --base-path=test/tmp/dest &>$DEST_GIT_DAEMON_OUTPUT & + DEST_GIT_DAEMON_PID=$! + + # wait for the git daemons to start before continuing + + if ! check_git_daemon $SRC_GIT_DAEMON_PORT; then + echo "timeout waiting for src git daemon to start" + exit 1 + fi + + if ! check_git_daemon $DEST_GIT_DAEMON_PORT; then + echo "timeout waiting for dest git daemon to start" + exit 1 + fi + + go run test/github.go \ + -p "$DEST_API_PORT" \ + -git-daemon-url "git://localhost:$DEST_GIT_DAEMON_PORT/" &>$DEST_API_OUTPUT & + DEST_API_PID=$! + + script/build + + until $(curl --output /dev/null --silent --head --fail "http://localhost:$DEST_API_PORT/ping"); do + sleep 0.1 + done + + set +e +} + +function after_suite() { + kill "$SRC_GIT_DAEMON_PID" "$DEST_GIT_DAEMON_PID" "$DEST_API_PID" +} + +function setup_src() { + rm -rf test/tmp/src + mkdir -p test/tmp/src + + for r in "$@"; do + local nwo=$(echo $r | cut -d':' -f1) + local refname=$(echo $r | cut -d':' -f2) + local sha=$(echo $r | cut -d':' -f3) + + local org=$(echo $nwo | cut -d'/' -f1) + mkdir -p test/tmp/src/$org && cp -R test/fixtures/repo.git test/tmp/src/$nwo + echo "$sha" >"test/tmp/src/$nwo/refs/$refname" + done +} + +function setup_cache() { + rm -rf test/tmp/cache + mkdir -p test/tmp/cache + + for r in "$@"; do + local nwo=$(echo $r | cut -d':' -f1) + local refname=$(echo $r | cut -d':' -f2) + local sha=$(echo $r | cut -d':' -f3) + mkdir -p test/tmp/cache/$nwo && cp -R test/fixtures/repo.git test/tmp/cache/$nwo/.git + echo "$sha" >"test/tmp/cache/$nwo/.git/refs/$refname" + done +} + +function setup_dest() { + rm -rf test/tmp/dest + mkdir -p test/tmp/dest + + for r in "$@"; do + local nwo=$(echo $r | cut -d':' -f1) + local refname=$(echo $r | cut -d':' -f2) + local sha=$(echo $r | cut -d':' -f3) + mkdir -p test/tmp/dest/$nwo && cp -R test/fixtures/repo.git test/tmp/dest/$nwo/.git + echo "$sha" >"test/tmp/dest/$nwo/.git/refs/$refname" + done +} + +function version() { + $ACTIONS_SYNC_BINARY version \ + &>$OUTPUT || + fail $1 +} + +function pull() { + $ACTIONS_SYNC_BINARY pull \ + --cache-dir "test/tmp/cache" \ + --source-url "git://localhost:$SRC_GIT_DAEMON_PORT" \ + "$1" "$2" \ + &>$OUTPUT || + fail $3 +} + +function push() { + $ACTIONS_SYNC_BINARY push \ + --cache-dir "test/tmp/cache" \ + --disable-push-git-auth \ + --destination-token "token" \ + --destination-url "http://localhost:$DEST_API_PORT" \ + &>$OUTPUT || + fail "$1" +} + +function push2args() { + $ACTIONS_SYNC_BINARY push \ + --cache-dir "test/tmp/cache" \ + --disable-push-git-auth \ + --destination-token "token" \ + --destination-url "http://localhost:$DEST_API_PORT" \ + "$1" "$2" \ + &>$OUTPUT || + fail $3 +} + +function push_impersonation() { + $ACTIONS_SYNC_BINARY push \ + --cache-dir "test/tmp/cache" \ + --disable-push-git-auth \ + --destination-token "token" \ + --destination-url "http://localhost:$DEST_API_PORT" \ + --actions-admin-user $1 \ + &>$OUTPUT || + fail "$2" +} + +function sync() { + $ACTIONS_SYNC_BINARY sync \ + --cache-dir "test/tmp/cache" \ + --source-url "git://localhost:$SRC_GIT_DAEMON_PORT" \ + --disable-push-git-auth \ + --destination-token "token" \ + --destination-url "http://localhost:$DEST_API_PORT" \ + "$1" "$2" \ + &>$OUTPUT || + fail $3 +} + +function assert_cache_sha() { + nwo=$1 + ref=$2 + expected=$3 + actual=$(cat "test/tmp/cache/$nwo/.git/refs/$ref") + [ "$actual" == "$expected" ] || fail "unexpected cache sha \`$expected != $actual\` - \`$nwo\` \`$ref\` - \`$4\`" +} + +function assert_dest_sha() { + nwo=$1 + ref=$2 + expected=$3 + actual=$(cat "test/tmp/dest/$nwo/.git/refs/$ref") + [ "$actual" == "$expected" ] || fail "unexpected dest sha \`$expected != $actual\` - \`$nwo\` \`$ref\` - \`$4\`" +} + +function fail() { + MSG=$1 + echo "FAIL: Failed $MSG" + echo -----output----- + cat $OUTPUT + echo -----/output----- + + echo -----github git daemon output----- + cat $SRC_GIT_DAEMON_OUTPUT + echo -----/github git daemon output----- + + echo -----ghes git daemon output----- + cat $DEST_GIT_DAEMON_OUTPUT + echo -----/ghes git daemon output----- + + echo -----ghes api output----- + cat $DEST_API_OUTPUT + echo -----/ghes api output----- + exit 1 +} + +function check_git_daemon { + port=$1 + for i in {1..10}; do + process=$(ps aux | grep "git daemon" | grep $port) + if [ -z "$process" ]; then + echo "Waiting for git daemon to start on port $port..." + sleep 1 + else + echo "Git daemon is running on port $port" + return 0 + fi + done + echo "Git daemon did not start within the expected time." + return 1 +} + +test_version +before_suite +test_pull +test_push +test_sync