diff --git a/.github/template.yml b/.github/template.yml index 6bc4323..128f303 100644 --- a/.github/template.yml +++ b/.github/template.yml @@ -1,8 +1,8 @@ template-repository: jebel-quant/sync_template template-branch: main -include: | - CODE_OF_CONDUCT.md - CONTRIBUTING.md +include: + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md exclude: - README.md - LICENSE diff --git a/action.yml b/action.yml index a9e5b06..55a92b8 100644 --- a/action.yml +++ b/action.yml @@ -1,220 +1,105 @@ -name: 'Sync Template' -description: 'Sync template into a project and optionally create a pull request' -author: 'Thomas Schmelzer' +name: Sync Repository Template +description: Synchronize a repository template into the current project and optionally open a pull request +author: Thomas Schmelzer inputs: token: - description: 'GitHub token or PAT for authentication' - required: true - source: - description: 'Path to the YAML configuration file containing template settings' + description: GitHub token or PAT for authentication required: true + branch: - description: 'Target branch in the current repo' - default: 'sync/update' + description: Target branch for the sync + required: false + default: sync/template-update + commit-message: - description: 'Commit message for sync' - default: 'chore: sync template' - test-mode: - description: 'If true, skip push and PR creation' - default: 'false' - automerge: - description: 'If true, enable auto-merge on the PR' - default: 'false' - template-ref: - description: 'Tag or branch to check out from the template repository (overrides template-branch)' + description: Commit message for the sync + required: false + default: "chore: sync template" + + create-pr: + description: Whether to create a pull request if changes are detected required: false + default: "true" outputs: changes-detected: - description: 'Whether changes were detected during the sync (true or false)' - value: ${{ steps.commit-changes.outputs.changes_detected }} + description: Whether changes were detected during the sync (true or false) + value: ${{ steps.sync.outputs.changes_detected }} runs: - using: "composite" + using: composite steps: # ------------------------------------------------------------ - # Checkout target repo + # Checkout target repository # ------------------------------------------------------------ - - name: Checkout the target repo + - name: Checkout repository uses: actions/checkout@v6 with: token: ${{ inputs.token }} fetch-depth: 0 - - name: Ensure inside a git repository - shell: bash - run: git rev-parse --is-inside-work-tree >/dev/null || { echo "❌ Not in a git repository"; exit 1; } - - # ------------------------------------------------------------ - # Parse configuration (repository, branch, ref, includes, excludes) - # ------------------------------------------------------------ - - name: Parse configuration file - shell: bash - id: config - run: | - CONFIG_FILE="${{ inputs.source }}" - echo "Reading configuration from ${CONFIG_FILE}" - - if [[ ! -f "${CONFIG_FILE}" ]]; then - echo "::error::Configuration file not found: ${CONFIG_FILE}" - exit 1 - fi - - # Install yq if needed - if ! command -v yq &>/dev/null; then - wget -qO /tmp/yq https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64 - chmod +x /tmp/yq - YQ="/tmp/yq" - else - YQ="yq" - fi - - TEMPLATE_REPO="$($YQ '.template-repository // ""' "${CONFIG_FILE}")" - if [[ -z "$TEMPLATE_REPO" ]]; then - echo "::error::template-repository missing in ${CONFIG_FILE}" - exit 1 - fi - echo "template_repository=$TEMPLATE_REPO" >> $GITHUB_OUTPUT - - TEMPLATE_BRANCH="$($YQ '.template-branch // "main"' "${CONFIG_FILE}")" - echo "template_branch=$TEMPLATE_BRANCH" >> $GITHUB_OUTPUT - - TEMPLATE_REF="$($YQ '.template-ref // ""' "${CONFIG_FILE}")" - echo "template_ref=$TEMPLATE_REF" >> $GITHUB_OUTPUT - - # New: Tag/Ref override - #TEMPLATE_REF_INPUT="${{ inputs.template-ref }}" - #TEMPLATE_REF_CONFIG="$($YQ '.template-ref // ""' "${CONFIG_FILE}")" - #TEMPLATE_REF="${TEMPLATE_REF_INPUT:-$TEMPLATE_REF_CONFIG}" - #echo "template_ref=$TEMPLATE_REF" >> $GITHUB_OUTPUT - - # Include patterns - support both pipe (|) and list (-) syntax - INCLUDE_TYPE="$($YQ '.include | type' "${CONFIG_FILE}" 2>/dev/null || echo "null")" - if [[ "$INCLUDE_TYPE" == "!!seq" ]]; then - # It's a YAML list with dashes - INCLUDE="$($YQ '.include[]' "${CONFIG_FILE}")" - else - # It's a string (pipe syntax) or doesn't exist - INCLUDE="$($YQ '.include // ""' "${CONFIG_FILE}")" - fi - echo "include<> $GITHUB_OUTPUT - echo "$INCLUDE" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - # Exclude patterns - support both pipe (|) and list (-) syntax - EXCLUDE_TYPE="$($YQ '.exclude | type' "${CONFIG_FILE}" 2>/dev/null || echo "null")" - if [[ "$EXCLUDE_TYPE" == "!!seq" ]]; then - # It's a YAML list with dashes - EXCLUDE="$($YQ '.exclude[]' "${CONFIG_FILE}")" - else - # It's a string (pipe syntax) or doesn't exist - EXCLUDE="$($YQ '.exclude // ""' "${CONFIG_FILE}")" - fi - echo "exclude<> $GITHUB_OUTPUT - echo "$EXCLUDE" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - # ------------------------------------------------------------ - # Sparse checkout template (branch or tag -> ref) + # Tooling # ------------------------------------------------------------ - - name: Sparse checkout template - uses: actions/checkout@v6 + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - repository: ${{ steps.config.outputs.template_repository }} - ref: ${{ steps.config.outputs.template_ref || steps.config.outputs.template_branch }} - path: .template-temp - token: ${{ inputs.token }} - fetch-depth: 0 - sparse-checkout: ${{ steps.config.outputs.include }} - sparse-checkout-cone-mode: false + version: "0.9.17" # ------------------------------------------------------------ - # Clean template and apply excludes + # Validate configuration # ------------------------------------------------------------ - - name: Clean template and apply excludes + - name: Validate rhiza configuration shell: bash - working-directory: .template-temp - run: | - rm -rf .git - EXCLUDES="${{ steps.config.outputs.exclude }}" - - if [[ -n "$EXCLUDES" ]]; then - echo "$EXCLUDES" | while IFS= read -r item; do - [[ -z "$item" ]] && continue - item="$(echo "$item" | xargs)" - rm -rf "$item" 2>/dev/null || true - done - fi - - tree -L 2 || ls -R + run: uvx rhiza validate . # ------------------------------------------------------------ - # Apply template + commit & push + # Materialize + commit changes # ------------------------------------------------------------ - - name: Commit and optionally push changes - id: commit-changes + - name: Sync template + id: sync shell: bash - env: - TEST_MODE: ${{ inputs.test-mode }} run: | - cp -R .template-temp/. . - rm -rf .template-temp + set -euo pipefail + + git checkout -B "${{ inputs.branch }}" + + uvx rhiza materialize . git add -A if git diff --cached --quiet; then - echo "changes_detected=false" >> $GITHUB_OUTPUT - echo "No changes." + echo "No changes detected." + echo "changes_detected=false" >> "$GITHUB_OUTPUT" exit 0 fi - echo "changes_detected=true" >> $GITHUB_OUTPUT + echo "changes_detected=true" >> "$GITHUB_OUTPUT" if git diff --cached --name-only | grep -q '^\.github/workflows/'; then - echo "⚠️ Workflow files modified — PAT with workflow scope required." + echo "⚠️ Workflow files modified — PAT with workflow scope may be required." fi - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m "${{ inputs.commit-message }}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - if [[ "$TEST_MODE" == "true" ]]; then - echo "🧪 Test mode: skipping push" - exit 0 - fi - - target_branch="${{ inputs.branch }}" - git push origin "HEAD:$target_branch" --force-with-lease + git commit -m "${{ inputs.commit-message }}" + git push origin "HEAD:${{ inputs.branch }}" --force-with-lease # ------------------------------------------------------------ # Create Pull Request # ------------------------------------------------------------ - - name: Create Pull Request - id: create-pr - if: ${{ inputs.test-mode != 'true' && steps.commit-changes.outputs.changes_detected == 'true' }} + - name: Create pull request + if: ${{ inputs.create-pr == 'true' && steps.sync.outputs.changes_detected == 'true' }} uses: peter-evans/create-pull-request@v8 with: token: ${{ inputs.token }} branch: ${{ inputs.branch }} - commit-message: ${{ inputs.commit-message }} delete-branch: false - title: "chore: sync template from ${{ steps.config.outputs.template_repository }}@${{ steps.config.outputs.template_ref || steps.config.outputs.template_branch }}" + title: ${{ inputs.commit-message }} body: | - This PR syncs the template from: - **${{ steps.config.outputs.template_repository }} @ ${{ steps.config.outputs.template_ref || steps.config.outputs.template_branch }}** + This pull request synchronizes the repository with its template. - # ------------------------------------------------------------ - # Auto-merge - # ------------------------------------------------------------ - - name: Enable auto-merge - if: ${{ inputs.automerge == 'true' && steps.create-pr.outputs.pull-request-number }} - shell: bash - env: - GH_TOKEN: ${{ inputs.token }} - run: | - gh pr merge ${{ steps.create-pr.outputs.pull-request-number }} \ - --merge \ - --auto \ - --delete-branch \ No newline at end of file + Changes were generated automatically using **rhiza**. diff --git a/tests/test-action.sh b/tests/test-action.sh index d7aeb9a..09c786a 100755 --- a/tests/test-action.sh +++ b/tests/test-action.sh @@ -1,251 +1,56 @@ #!/bin/bash -set -e +set -euo pipefail -# Colors for output +# ------------------------------------------------------------ +# Colors +# ------------------------------------------------------------ GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m' -NC='\033[0m' # No Color +NC='\033[0m' -echo -e "${YELLOW}Starting test for Sync Template Action${NC}" +echo -e "${YELLOW}Starting test for Sync Repository Template action${NC}" -# Create test directory -TEST_DIR=$(mktemp -d) -echo -e "Using temporary directory: ${TEST_DIR}" - -# Cleanup function -cleanup() { - echo -e "${YELLOW}Cleaning up test environment${NC}" - rm -rf "${TEST_DIR}" -} - -# Register cleanup function to run on exit -trap cleanup EXIT - -# Function to check if a test passes -assert() { - local condition=$1 - local message=$2 - - if eval "${condition}"; then - echo -e "${GREEN}✅ PASS: ${message}${NC}" - else - echo -e "${RED}❌ FAIL: ${message}${NC}" - exit 1 - fi -} - -# Setup source repository (template) -echo -e "${YELLOW}Setting up source repository${NC}" -SOURCE_REPO="${TEST_DIR}/source-repo" -mkdir -p "${SOURCE_REPO}" -cd "${SOURCE_REPO}" - -git init -git config user.name "Test User" -git config user.email "test@example.com" - -# Create template files -echo "# Template README" > README.md -echo "# Code of Conduct" > CODE_OF_CONDUCT.md -echo "# Contributing Guide" > CONTRIBUTING.md -mkdir -p .github/workflows -echo "name: Test Workflow" > .github/workflows/test.yml -echo "# License" > LICENSE - -git add . -git commit -m "Initial commit" - -# Create a main branch (default in newer repos) -git branch -m main - -# Setup target repository -echo -e "${YELLOW}Setting up target repository${NC}" -TARGET_REPO="${TEST_DIR}/target-repo" -mkdir -p "${TARGET_REPO}" -cd "${TARGET_REPO}" - -git init -git config user.name "Test User" -git config user.email "test@example.com" - -# Create initial files in target repo -echo "# Target README" > README.md -echo "# Target License" > LICENSE -git add . -git commit -m "Initial commit" - -# Create test branch -git checkout -b sync/update-configs - -# Simulate running the action -echo -e "${YELLOW}Simulating action execution${NC}" -# Save the branch name to ensure we stay on it -BRANCH_NAME="sync/update-configs" - -# Create template.yml file -echo "Creating template.yml file" -cat > template.yml << EOF -template-repository: ${SOURCE_REPO} -template-branch: main -include: | - CODE_OF_CONDUCT.md - CONTRIBUTING.md - .github/ -exclude: | - README.md - LICENSE -EOF - -# Step 1: Sparse checkout from template repo -echo "Performing sparse checkout" -git remote add template "${SOURCE_REPO}" -git fetch template - -# Create a temporary directory for template files -mkdir -p .template-temp -cd .template-temp - -# Clone specific files from template -git init -git config user.name "Test User" -git config user.email "test@example.com" -git remote add origin "${SOURCE_REPO}" -git config core.sparseCheckout true - -# Read include patterns from template.yml file -while IFS= read -r pattern; do - pattern="$(echo "$pattern" | xargs)" - [ -z "$pattern" ] || echo "$pattern" >> .git/info/sparse-checkout -done < <(grep -A 10 "^include: |" ../template.yml | tail -n +2 | grep -v "^exclude:") - -git pull origin main --depth=1 - -# Step 2: Apply excludes -echo "Applying excludes" -# First, remove the .git directory -rm -rf .git - -# Read exclude patterns from template.yml file and remove files -while IFS= read -r pattern; do - pattern="$(echo "$pattern" | xargs)" - [ -z "$pattern" ] || rm -rf "$pattern" 2>/dev/null || true -done < <(grep -A 10 "^exclude: |" ../template.yml | tail -n +2) - -# Make sure we're on the sync/update-configs branch before copying files -cd "${TARGET_REPO}" -git checkout sync/update-configs - -# Step 3: Copy template files to target repo -echo "Copying template files" -# Copy all files from template temp directory -cp -R .template-temp/. . -rm -rf .template-temp - -# Step 4: Commit and push changes -git checkout "${BRANCH_NAME}" -git add CODE_OF_CONDUCT.md CONTRIBUTING.md .github -git commit -m "chore: sync template" - -# Verify results -echo -e "${YELLOW}Verifying results${NC}" - -# Check that files were synced correctly -assert "[ -f CODE_OF_CONDUCT.md ]" "CODE_OF_CONDUCT.md exists" -assert "[ -f CONTRIBUTING.md ]" "CONTRIBUTING.md exists" -assert "[ -d .github ]" ".github directory exists" -assert "[ -f .github/workflows/test.yml ]" "Workflow file exists" - -# Check that excluded files were not synced -assert "[ \"$(cat README.md)\" = \"# Target README\" ]" "README.md was not overwritten" -assert "[ \"$(cat LICENSE)\" = \"# Target License\" ]" "LICENSE was not overwritten" - -# Check commit message -assert "[ \"$(git log -1 --pretty=%B)\" = \"chore: sync template\" ]" "Commit message is correct" - -# Simulate auto-merge behavior as implemented in action.yml -# Condition: Enable auto-merge only when automerge input is 'true' AND a PR number is available (non-empty) -echo -e "${YELLOW}Simulating auto-merge behavior${NC}" - -# Positive case: automerge enabled and PR number available triggers auto-merge command -AUTOMERGE="true" -PR_NUMBER="123" -AUTO_MERGE_CMD="" -if [[ "${AUTOMERGE}" == "true" && "${PR_NUMBER}" != "" ]]; then - AUTO_MERGE_CMD="gh pr merge ${PR_NUMBER} --merge --auto --delete-branch" -fi -assert "[ \"${AUTO_MERGE_CMD}\" = \"gh pr merge 123 --merge --auto --delete-branch\" ]" "Auto-merge command generated when automerge is true and PR number is set" - -# Negative case: automerge disabled means skip even if PR number exists -AUTOMERGE="false" -PR_NUMBER="123" -AUTO_MERGE_EXECUTED="false" -if [[ "${AUTOMERGE}" == "true" && "${PR_NUMBER}" != "" ]]; then - AUTO_MERGE_EXECUTED="true" -fi -assert "[ \"${AUTO_MERGE_EXECUTED}\" = \"false\" ]" "Auto-merge is skipped when automerge is false even if PR number is provided" - -# Negative case: automerge enabled but no PR number means skip -AUTOMERGE="true" -PR_NUMBER="" -AUTO_MERGE_EXECUTED="false" -if [[ "${AUTOMERGE}" == "true" && "${PR_NUMBER}" != "" ]]; then - AUTO_MERGE_EXECUTED="true" +# ------------------------------------------------------------ +# Ensure uv / uvx is available (mirror action) +# ------------------------------------------------------------ +if ! command -v uvx >/dev/null 2>&1; then + echo -e "${YELLOW}Installing uv / uvx${NC}" + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" fi -assert "[ \"${AUTO_MERGE_EXECUTED}\" = \"false\" ]" "Auto-merge is skipped when PR number is empty" -# Test list format for include/exclude -echo -e "${YELLOW}Testing YAML list format for include/exclude${NC}" - -# Setup a new test with list format -cd "${TARGET_REPO}" -# Clean up previous test files -git checkout main 2>/dev/null || git checkout -b main -git reset --hard HEAD~1 2>/dev/null || true -git checkout -b sync/test-list-format - -# Create template.yml with list format -cat > template-list.yml << EOF -template-repository: ${SOURCE_REPO} -template-branch: main -include: - - CODE_OF_CONDUCT.md - - CONTRIBUTING.md -exclude: - - README.md -EOF - -# Install yq if needed -if ! command -v yq &>/dev/null; then - wget -qO /tmp/yq https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64 - chmod +x /tmp/yq - YQ="/tmp/yq" -else - YQ="yq" -fi - -# Parse the list format using same logic as action.yml -INCLUDE_TYPE="$($YQ '.include | type' template-list.yml 2>/dev/null || echo "null")" -if [[ "$INCLUDE_TYPE" == "!!seq" ]]; then - INCLUDE="$($YQ '.include[]' template-list.yml)" -else - INCLUDE="$($YQ '.include // ""' template-list.yml)" -fi +## ------------------------------------------------------------ +## Clone the jebel-quant/rhiza repository to test with +## ------------------------------------------------------------ +#TEST_DIR="$(mktemp -d)" +#trap 'rm -rf "${TEST_DIR}"' EXIT +#echo "Using temp dir: ${TEST_DIR}" + +#echo -e "${YELLOW}Cloning tschm/monkeys repository${NC}" +#cd "${TEST_DIR}" +#git clone --quiet https://github.com/jebel-quant/sync_template.git +#cd sync_template + +# ------------------------------------------------------------ +# Run rhiza commands (mirror what the action does) +# ------------------------------------------------------------ +echo -e "${YELLOW}Running rhiza validate${NC}" +uvx rhiza validate . + +echo -e "${YELLOW}Running rhiza materialize${NC}" +# git checkout -B sync/template-update +uvx rhiza materialize . + +# ------------------------------------------------------------ +# Verify basic functionality +# ------------------------------------------------------------ +echo -e "${YELLOW}Verifying results${NC}" -EXCLUDE_TYPE="$($YQ '.exclude | type' template-list.yml 2>/dev/null || echo "null")" -if [[ "$EXCLUDE_TYPE" == "!!seq" ]]; then - EXCLUDE="$($YQ '.exclude[]' template-list.yml)" +if git diff --cached --quiet && git diff --quiet; then + echo -e "${GREEN}✅ PASS: rhiza commands executed successfully${NC}" else - EXCLUDE="$($YQ '.exclude // ""' template-list.yml)" + echo -e "${YELLOW}ℹ️ Changes detected (this is expected if template has updates)${NC}" fi -# Verify parsed values -assert "[ \"$INCLUDE_TYPE\" = \"!!seq\" ]" "Include field is recognized as a sequence" -assert "[ \"$EXCLUDE_TYPE\" = \"!!seq\" ]" "Exclude field is recognized as a sequence" - -# Check that parsed values contain expected content -assert "echo \"$INCLUDE\" | grep -q 'CODE_OF_CONDUCT.md'" "Include contains CODE_OF_CONDUCT.md" -assert "echo \"$INCLUDE\" | grep -q 'CONTRIBUTING.md'" "Include contains CONTRIBUTING.md" -assert "echo \"$EXCLUDE\" | grep -q 'README.md'" "Exclude contains README.md" - -echo -e "${GREEN}All tests passed!${NC}" \ No newline at end of file +echo -e "${GREEN}All tests passed!${NC}"