diff --git a/.github/actions/test-assertions/action.yml b/.github/actions/test-assertions/action.yml index 1908dea..2bf7716 100644 --- a/.github/actions/test-assertions/action.yml +++ b/.github/actions/test-assertions/action.yml @@ -39,7 +39,7 @@ runs: test_site_repo_token: ${{ inputs.test_site_repo_token }} - name: Assert the state after running the action - shell: sh + shell: bash working-directory: test run: | # Assert the state after running the action diff --git a/.github/actions/test-checkout/action.yml b/.github/actions/test-checkout/action.yml index aa5713f..57e4d3e 100644 --- a/.github/actions/test-checkout/action.yml +++ b/.github/actions/test-checkout/action.yml @@ -22,14 +22,14 @@ inputs: runs: using: composite steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: ${{ inputs.test_site_repo }} token: ${{ inputs.test_site_repo_token }} path: test/site fetch-depth: 0 - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: ${{ inputs.test_config_repo }} token: ${{ inputs.test_config_repo_token }} diff --git a/.github/actions/test-setup/action.yml b/.github/actions/test-setup/action.yml index 866f3da..c24e388 100644 --- a/.github/actions/test-setup/action.yml +++ b/.github/actions/test-setup/action.yml @@ -73,8 +73,11 @@ runs: git add . git commit -m "Add initial mock config" echo modified > c - touch e - git add c e + echo baseline > e + mkdir -p language/en + echo subdir-config > language/en/system.site.yml + echo dotfile-config > .htaccess + git add c e language .htaccess git rm d git commit -m "Updated" git push origin HEAD:${{ inputs.test_config_repo_branch }} @@ -87,7 +90,7 @@ runs: git checkout --orphan ${{ inputs.test_site_repo_pr_branch_base }}-new git rm -rf . mkdir -p a z config/sync - cp -r ../remote/* config/sync + find ../remote -mindepth 1 -maxdepth 1 ! -name .git -exec cp -a {} config/sync \; touch {a,z}/.gitkeep b git add . git commit -m "Add mock Drupal site" diff --git a/.github/workflows/test-script.yml b/.github/workflows/test-script.yml new file mode 100644 index 0000000..75842f0 --- /dev/null +++ b/.github/workflows/test-script.yml @@ -0,0 +1,25 @@ +name: Test Drupal host script + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + workflow_dispatch: + +jobs: + test-script: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Cache BATS + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + with: + path: .bats + key: bats-${{ hashFiles('bats.lock') }} + + - run: make test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0065305..2be892d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,8 +3,10 @@ name: Test # workflow-templates/update-config-branch.yml instead. # # Secrets -# - TEST_CONFIG_REPO_TOKEN: A token with write access to the test config repo. -# - TEST_SITE_REPO_TOKEN: A token with write access to the test site repo. +# - TEST_CONFIG_REPO_TOKEN: A token with write access to the contents of the +# config repo. +# - TEST_SITE_REPO_TOKEN: A token with write access to the contents and PRs of +# the test site repo. # Variables # - TEST_CONFIG_REPO: The GitHub config repo for use with the test. # - TEST_SITE_REPO: The GitHub site repo for use with the test. @@ -29,7 +31,6 @@ concurrency: jobs: test-initial-setup: -# if: false name: Test mirroring the config repo to the site repo (ie initial setup) runs-on: ubuntu-latest concurrency: @@ -42,7 +43,7 @@ jobs: test_site_repo_pr_branch_base: "${{ github.job }}-staging" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: test/update-config @@ -98,8 +99,137 @@ jobs: with: branch_prefix: "${{ github.job }}-" + test-initial-setup-creates-pr: + name: Test that initial setup opens a PR when prod config diverges from live. + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-job + env: + test_config_repo_branch: "${{ github.job }}-main" + test_site_repo_config_branch: "${{ github.job }}-config-only" + test_site_repo_live_branch: "${{ github.job }}-main" + test_site_repo_pr_branch: "${{ github.job }}-automatic-config-export" + test_site_repo_pr_branch_base: "${{ github.job }}-staging" + steps: + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + path: test/update-config + + - uses: ./test/update-config/.github/actions/test-setup + with: + branch_prefix: "${{ github.job }}-" + test_config_repo: ${{ vars.TEST_CONFIG_REPO }} + test_config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} + test_config_repo_branch: ${{ env.test_config_repo_branch }} + test_site_repo: ${{ vars.TEST_SITE_REPO }} + test_site_repo_token: ${{ secrets.TEST_SITE_REPO_TOKEN }} + test_site_repo_config_branch: ${{ env.test_site_repo_config_branch }} + test_site_repo_live_branch: ${{ env.test_site_repo_live_branch }} + test_site_repo_pr_branch: ${{ env.test_site_repo_pr_branch }} + test_site_repo_pr_branch_base: ${{ env.test_site_repo_pr_branch_base }} + + - name: 'Test setup: diverge prod config from live before the mirror branch exists' + working-directory: test + run: | + # Diverge prod config from the live branch, leaving the mirror branch absent. + set -eu + cd remote + echo firstrun > c + git add c + git commit -m "Prod config change before mirror branch exists" + git push origin HEAD:${{ env.test_config_repo_branch }} + + # Leave an obsolete config subdirectory in live to prove subdir removal. + cd ../site + git checkout ${{ env.test_site_repo_live_branch }} + mkdir -p config/sync/obsolete_collection + echo stale > config/sync/obsolete_collection/legacy.yml + git add config/sync + git commit -m "Leave an obsolete config subdirectory in live" + git push origin HEAD + + - uses: ./test/update-config + id: run_action + with: + config_repo: ${{ vars.TEST_CONFIG_REPO }} + config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} + config_repo_branch: ${{ env.test_config_repo_branch }} + site_repo: ${{ vars.TEST_SITE_REPO }} + site_repo_token: ${{ secrets.TEST_SITE_REPO_TOKEN }} + site_repo_config_branch: ${{ env.test_site_repo_config_branch }} + site_repo_live_branch: ${{ env.test_site_repo_live_branch }} + site_repo_pr_branch: ${{ env.test_site_repo_pr_branch }} + site_repo_pr_branch_base: ${{ env.test_site_repo_pr_branch_base }} + + - uses: ./test/update-config/.github/actions/test-assertions + with: + test_config_repo: ${{ vars.TEST_CONFIG_REPO }} + test_config_repo_branch: ${{ env.test_config_repo_branch }} + test_config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} + test_site_repo: ${{ vars.TEST_SITE_REPO }} + test_site_repo_token: ${{ secrets.TEST_SITE_REPO_TOKEN }} + test_site_repo_config_branch: ${{ env.test_site_repo_config_branch }} + + - name: Assert a PR was created carrying the prod config + env: + GH_TOKEN: ${{ secrets.TEST_SITE_REPO_TOKEN }} + working-directory: test/site + run: | + # Assert a PR was created carrying the prod config + set -eu + exit=0 + + if [[ "$(gh pr list --head ${{ env.test_site_repo_pr_branch }} --json id)" == "[]" ]]; then + echo "**TEST FAILURE:** A PR should be created on initial setup when prod config diverges from live." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + git fetch origin ${{ env.test_site_repo_pr_branch }} + git checkout origin/${{ env.test_site_repo_pr_branch }} -- config/sync + if [[ "$(cat config/sync/c)" != "firstrun" ]]; then + echo "**TEST FAILURE:** The PR branch should carry the prod config (config/sync/c should be 'firstrun')." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + ref=origin/${{ env.test_site_repo_pr_branch }} + tree=$(git ls-tree -r --name-only "$ref" -- config/sync) + if [[ "$(git show "$ref:config/sync/language/en/system.site.yml")" != "subdir-config" ]]; then + echo "**TEST FAILURE:** config/sync/language/en/system.site.yml should be 'subdir-config' (config subdirectory not copied)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + if [[ "$(git show "$ref:config/sync/.htaccess")" != "dotfile-config" ]]; then + echo "**TEST FAILURE:** config/sync/.htaccess should be 'dotfile-config' (dotfile not copied)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + if grep -q '^config/sync/obsolete_collection/' <<<"$tree"; then + echo "**TEST FAILURE:** config/sync/obsolete_collection/ should have been removed (stale subdirectory not deleted)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + if grep -q '^config/sync/\.git/' <<<"$tree"; then + echo "**TEST FAILURE:** config/sync/.git/ should not be present (remote .git was copied)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + exit $exit + + - name: Assert the summary names the PR branch, not the config branch + env: + SUMMARY: ${{ steps.run_action.outputs.summary }} + run: | + set -eu + printf '%s' "$SUMMARY" | grep -qF "${{ env.test_site_repo_pr_branch }}" || { + echo "**TEST FAILURE:** Summary should name the PR branch (\`${{ env.test_site_repo_pr_branch }}\`), was: $SUMMARY" >> "$GITHUB_STEP_SUMMARY"; exit 1; } + if printf '%s' "$SUMMARY" | grep -qF "${{ env.test_site_repo_config_branch }}"; then + echo "**TEST FAILURE:** Summary should not name the config-only branch as the commit target, was: $SUMMARY" >> "$GITHUB_STEP_SUMMARY"; exit 1 + fi + + - uses: ./test/update-config/.github/actions/test-teardown + if: ${{ always() }} + with: + branch_prefix: "${{ github.job }}-" + test-create-pr: -# if: false name: Test updating the mirrored config branch and creating a PR from it. runs-on: ubuntu-latest env: @@ -110,7 +240,7 @@ jobs: test_site_repo_live_branch: "${{ github.job }}-main" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: test/update-config @@ -142,8 +272,9 @@ jobs: # Ensure the live branch matches the new site repo config branch. git checkout ${{ env.test_site_repo_live_branch }} - rm config/sync/* + find config/sync -mindepth 1 -delete mv "$tmp_dir"/* config/sync + echo baseline > config/sync/e git add config/sync git commit --amend -m "Update" git push --force origin HEAD @@ -157,6 +288,7 @@ jobs: git push --force - uses: ./test/update-config + id: run_action with: config_repo: ${{ vars.TEST_CONFIG_REPO }} config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} @@ -184,24 +316,190 @@ jobs: run: | set -eu exit=0 - + if [[ "$(gh pr list --head ${{ env.test_site_repo_pr_branch }} --json id)" == "[]" ]]; then echo "**TEST FAILURE:** There should be a PR created." >> "$GITHUB_STEP_SUMMARY" exit=1 fi - + git checkout ${{ env.test_site_repo_pr_branch_base }} - git merge origin/${{ env.test_site_repo_pr_branch }} + if ! git merge --no-edit origin/${{ env.test_site_repo_pr_branch }}; then + echo "**TEST FAILURE:** Merging the config PR into \`${{ env.test_site_repo_pr_branch_base }}\` produced a conflict." >> "$GITHUB_STEP_SUMMARY" + git merge --abort || true + exit 1 + fi if [[ "$(cat config/sync/c)" != "modified" ]]; then echo "**TEST FAILURE:** After merging the config PR the 'c' file is incorrect." >> "$GITHUB_STEP_SUMMARY" exit=1 fi - + if [[ "$(cat config/sync/e)" != "modified" ]]; then echo "**TEST FAILURE:** After merging the config PR the 'e' file is incorrect." >> "$GITHUB_STEP_SUMMARY" exit=1 fi - + + if [[ ! -f config/sync/language/en/system.site.yml ]]; then + echo "**TEST FAILURE:** config/sync/language/en/system.site.yml should be present after merging the config PR (config subdirectory lost)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + if [[ ! -f config/sync/.htaccess ]]; then + echo "**TEST FAILURE:** config/sync/.htaccess should be present after merging the config PR (dotfile lost)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + exit $exit + + - name: Assert the summary names the PR branch, not the config branch + env: + SUMMARY: ${{ steps.run_action.outputs.summary }} + run: | + set -eu + printf '%s' "$SUMMARY" | grep -qF "${{ env.test_site_repo_pr_branch }}" || { + echo "**TEST FAILURE:** Summary should name the PR branch (\`${{ env.test_site_repo_pr_branch }}\`), was: $SUMMARY" >> "$GITHUB_STEP_SUMMARY"; exit 1; } + if printf '%s' "$SUMMARY" | grep -qF "${{ env.test_site_repo_config_branch }}"; then + echo "**TEST FAILURE:** Summary should not name the config-only branch as the commit target, was: $SUMMARY" >> "$GITHUB_STEP_SUMMARY"; exit 1 + fi + + - uses: ./test/update-config/.github/actions/test-teardown + if: ${{ always() }} + with: + branch_prefix: "${{ github.job }}-" + + test-stale-closed-pr: + name: Test that a leftover branch from a closed/rejected PR is reset, not stacked onto. + runs-on: ubuntu-latest + env: + test_config_repo_branch: "${{ github.job }}-main" + test_site_repo_config_branch: "${{ github.job }}-config-only" + test_site_repo_pr_branch: "${{ github.job }}-automatic-config-export" + test_site_repo_pr_branch_base: "${{ github.job }}-staging" + test_site_repo_live_branch: "${{ github.job }}-main" + steps: + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + path: test/update-config + + - uses: ./test/update-config/.github/actions/test-setup + with: + branch_prefix: "${{ github.job }}-" + test_config_repo: ${{ vars.TEST_CONFIG_REPO }} + test_config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} + test_config_repo_branch: ${{ env.test_config_repo_branch }} + test_site_repo: ${{ vars.TEST_SITE_REPO }} + test_site_repo_token: ${{ secrets.TEST_SITE_REPO_TOKEN }} + test_site_repo_config_branch: ${{ env.test_site_repo_config_branch }} + test_site_repo_live_branch: ${{ env.test_site_repo_live_branch }} + test_site_repo_pr_branch: ${{ env.test_site_repo_pr_branch }} + test_site_repo_pr_branch_base: ${{ env.test_site_repo_pr_branch_base }} + + - name: 'Test setup: mirror behind, align live, and leave a stale closed PR branch' + env: + GH_TOKEN: ${{ secrets.TEST_SITE_REPO_TOKEN }} + working-directory: test + run: | + set -eu + cd site + git remote add remote-config ../remote + git fetch remote-config + # Create the site repo config-only branch 1 commit behind the config repo. + git checkout -b ${{ env.test_site_repo_config_branch }} remote-config/${{ env.test_config_repo_branch }}^ + tmp_dir=$(mktemp -d) + cp * "$tmp_dir" + git push origin HEAD + + # Ensure the live branch matches the new site repo config branch. + git checkout ${{ env.test_site_repo_live_branch }} + find config/sync -mindepth 1 -delete + mv "$tmp_dir"/* config/sync + echo baseline > config/sync/e + git add config/sync + # A plain commit (not --amend): the live branch tip is the orphan root + # commit that staging also descends from. Amending it would create a + # new root with no history in common with staging, breaking `gh pr + # create` below (unlike the job above, this test never resets staging + # onto live). + git commit -m "Update" + git push --force origin HEAD + + # Leave behind a stale rejected PR branch from a previously closed PR. + git checkout -b ${{ env.test_site_repo_pr_branch }} ${{ env.test_site_repo_live_branch }} + echo rejected > config/sync/rejected + git add config/sync + git commit -m "Rejected config change from a previous PR" + git push origin HEAD + gh pr create \ + --base ${{ env.test_site_repo_pr_branch_base }} \ + --head ${{ env.test_site_repo_pr_branch }} \ + --title "Old rejected PR" \ + --body "Simulated previously rejected config PR." + # Closes without --delete-branch, so origin/PR_BRANCH remains. + gh pr close ${{ env.test_site_repo_pr_branch }} + + - uses: ./test/update-config + id: run_action + with: + config_repo: ${{ vars.TEST_CONFIG_REPO }} + config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} + config_repo_branch: ${{ env.test_config_repo_branch }} + site_repo: ${{ vars.TEST_SITE_REPO }} + site_repo_token: ${{ secrets.TEST_SITE_REPO_TOKEN }} + site_repo_config_branch: ${{ env.test_site_repo_config_branch }} + site_repo_live_branch: ${{ env.test_site_repo_live_branch }} + site_repo_pr_branch: ${{ env.test_site_repo_pr_branch }} + site_repo_pr_branch_base: ${{ env.test_site_repo_pr_branch_base }} + + - uses: ./test/update-config/.github/actions/test-assertions + with: + test_config_repo: ${{ vars.TEST_CONFIG_REPO }} + test_config_repo_branch: ${{ env.test_config_repo_branch }} + test_config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} + test_site_repo: ${{ vars.TEST_SITE_REPO }} + test_site_repo_token: ${{ secrets.TEST_SITE_REPO_TOKEN }} + test_site_repo_config_branch: ${{ env.test_site_repo_config_branch }} + + - name: Assert the stale rejected history is discarded and a single fresh PR exists + env: + GH_TOKEN: ${{ secrets.TEST_SITE_REPO_TOKEN }} + working-directory: test/site + run: | + set -eu + exit=0 + + pr_count=$(gh pr list --head ${{ env.test_site_repo_pr_branch }} --base ${{ env.test_site_repo_pr_branch_base }} --state open --json id --jq 'length') + if [[ "$pr_count" != "1" ]]; then + echo "**TEST FAILURE:** Expected exactly 1 open PR for the PR branch, found $pr_count." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + git fetch origin ${{ env.test_site_repo_live_branch }} ${{ env.test_site_repo_pr_branch }} + + # The PR branch must be exactly one commit ahead of live: the rejected + # commit from the closed PR must not survive. (This is the assertion the + # buggy code fails.) + count=$(git rev-list --count origin/${{ env.test_site_repo_live_branch }}..origin/${{ env.test_site_repo_pr_branch }}) + if [[ "$count" != "1" ]]; then + echo "**TEST FAILURE:** The PR branch should be exactly 1 commit ahead of live, was $count (stale rejected history leaked in)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + if git log origin/${{ env.test_site_repo_pr_branch }} --format='%s' | grep -q "Rejected config change"; then + echo "**TEST FAILURE:** The rejected commit from the closed PR reappeared in the new PR history." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + # Sanity: the resurfaced file is gone and the current prod config is present. + git checkout origin/${{ env.test_site_repo_pr_branch }} -- config/sync + if [[ -f config/sync/rejected ]]; then + echo "**TEST FAILURE:** config/sync/rejected should not be present in the new PR branch." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + if [[ "$(cat config/sync/c)" != "modified" ]]; then + echo "**TEST FAILURE:** config/sync/c should carry the prod value 'modified'." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + exit $exit - uses: ./test/update-config/.github/actions/test-teardown @@ -210,7 +508,6 @@ jobs: branch_prefix: "${{ github.job }}-" test-existing-pr: -# if: false name: Test updating an existing PR. runs-on: ubuntu-latest env: @@ -221,7 +518,7 @@ jobs: test_site_repo_pr_branch_base: "${{ github.job }}-staging" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: test/update-config @@ -255,7 +552,7 @@ jobs: # Set up an existing PR. git checkout ${{ env.test_site_repo_pr_branch_base }} - rm config/sync/* + find config/sync -mindepth 1 -delete touch config/sync/test-existing-pr git add config/sync git commit --amend -m "Initial commmit" @@ -272,6 +569,7 @@ jobs: rm -rf "$tmp_dir" - uses: ./test/update-config + id: run_action with: config_repo: ${{ vars.TEST_CONFIG_REPO }} config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} @@ -297,20 +595,150 @@ jobs: GH_TOKEN: ${{ secrets.TEST_SITE_REPO_TOKEN }} working-directory: test/site run: | - # Assert the state after running the action + # Assert the existing PR was updated and not duplicated set -eu - if [[ "$(gh pr list -H ${{ env.test_site_repo_pr_branch }} --json id)" == "[]" ]]; then - echo "**TEST FAILURE:** There should still be an existing PR." >> "$GITHUB_STEP_SUMMARY" - exit 1 + exit=0 + + git checkout origin/${{ env.test_site_repo_pr_branch }} -- config/sync + + if [[ "$(cat config/sync/c)" != "modified" ]]; then + echo "**TEST FAILURE:** config/sync/c should be 'modified' (new config from HEAD)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + if [[ ! -f config/sync/e ]]; then + echo "**TEST FAILURE:** config/sync/e should be present (from config repo HEAD)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + if [[ -f config/sync/test-existing-pr ]]; then + echo "**TEST FAILURE:** config/sync/test-existing-pr should be gone (stale pre-action file)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + if [[ -f config/sync/d ]]; then + echo "**TEST FAILURE:** config/sync/d should be gone (stale pre-action file)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + git fetch origin ${{ env.test_site_repo_pr_branch }} + if [[ "$(git show "origin/${{ env.test_site_repo_pr_branch }}:config/sync/language/en/system.site.yml")" != "subdir-config" ]]; then + echo "**TEST FAILURE:** config/sync/language/en/system.site.yml should be 'subdir-config' (config subdirectory not copied on update)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + if [[ "$(git show "origin/${{ env.test_site_repo_pr_branch }}:config/sync/.htaccess")" != "dotfile-config" ]]; then + echo "**TEST FAILURE:** config/sync/.htaccess should be 'dotfile-config' (dotfile not copied on update)." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + pr_count=$(gh pr list --head ${{ env.test_site_repo_pr_branch }} --json id --jq 'length') + if [[ "$pr_count" != "1" ]]; then + echo "**TEST FAILURE:** Expected exactly 1 open PR, found $pr_count." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + exit $exit + + - name: Assert the summary names the PR branch, not the config branch + env: + SUMMARY: ${{ steps.run_action.outputs.summary }} + run: | + set -eu + printf '%s' "$SUMMARY" | grep -qF "${{ env.test_site_repo_pr_branch }}" || { + echo "**TEST FAILURE:** Summary should name the PR branch (\`${{ env.test_site_repo_pr_branch }}\`), was: $SUMMARY" >> "$GITHUB_STEP_SUMMARY"; exit 1; } + if printf '%s' "$SUMMARY" | grep -qF "${{ env.test_site_repo_config_branch }}"; then + echo "**TEST FAILURE:** Summary should not name the config-only branch as the commit target, was: $SUMMARY" >> "$GITHUB_STEP_SUMMARY"; exit 1 + fi + + - uses: ./test/update-config/.github/actions/test-teardown + if: ${{ always() }} + with: + branch_prefix: "${{ github.job }}-" + + test-already-in-sync: + name: Test the steady-state no-op (mirror already at config HEAD, so do nothing). + runs-on: ubuntu-latest + env: + test_config_repo_branch: "${{ github.job }}-main" + test_site_repo_config_branch: "${{ github.job }}-config-only" + test_site_repo_pr_branch: "${{ github.job }}-automatic-config-export" + test_site_repo_pr_branch_base: "${{ github.job }}-staging" + test_site_repo_live_branch: "${{ github.job }}-main" + steps: + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + path: test/update-config + + - uses: ./test/update-config/.github/actions/test-setup + with: + branch_prefix: "${{ github.job }}-" + test_config_repo: ${{ vars.TEST_CONFIG_REPO }} + test_config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} + test_config_repo_branch: ${{ env.test_config_repo_branch }} + test_site_repo: ${{ vars.TEST_SITE_REPO }} + test_site_repo_token: ${{ secrets.TEST_SITE_REPO_TOKEN }} + test_site_repo_config_branch: ${{ env.test_site_repo_config_branch }} + test_site_repo_live_branch: ${{ env.test_site_repo_live_branch }} + test_site_repo_pr_branch: ${{ env.test_site_repo_pr_branch }} + test_site_repo_pr_branch_base: ${{ env.test_site_repo_pr_branch_base }} + + - name: 'Test setup: create mirror branch at config HEAD (not behind)' + working-directory: test/site + run: | + set -eu + git remote add remote-config ../remote + git fetch remote-config + git checkout -b ${{ env.test_site_repo_config_branch }} remote-config/${{ env.test_config_repo_branch }} + git push origin HEAD + + - uses: ./test/update-config + with: + config_repo: ${{ vars.TEST_CONFIG_REPO }} + config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} + config_repo_branch: ${{ env.test_config_repo_branch }} + site_repo: ${{ vars.TEST_SITE_REPO }} + site_repo_token: ${{ secrets.TEST_SITE_REPO_TOKEN }} + site_repo_config_branch: ${{ env.test_site_repo_config_branch }} + site_repo_live_branch: ${{ env.test_site_repo_live_branch }} + site_repo_pr_branch: ${{ env.test_site_repo_pr_branch }} + site_repo_pr_branch_base: ${{ env.test_site_repo_pr_branch_base }} + + - uses: ./test/update-config/.github/actions/test-assertions + with: + test_config_repo: ${{ vars.TEST_CONFIG_REPO }} + test_config_repo_branch: ${{ env.test_config_repo_branch }} + test_config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} + test_site_repo: ${{ vars.TEST_SITE_REPO }} + test_site_repo_token: ${{ secrets.TEST_SITE_REPO_TOKEN }} + test_site_repo_config_branch: ${{ env.test_site_repo_config_branch }} + + - name: Assert no PR was created and PR branch was never pushed + env: + GH_TOKEN: ${{ secrets.TEST_SITE_REPO_TOKEN }} + working-directory: test/site + run: | + set -eu + exit=0 + + if [[ "$(gh pr list --head ${{ env.test_site_repo_pr_branch }} --json id)" != "[]" ]]; then + echo "**TEST FAILURE:** No PR should have been created on the early-out path." >> "$GITHUB_STEP_SUMMARY" + exit=1 + fi + + if git ls-remote --heads origin "${{ env.test_site_repo_pr_branch }}" | grep -q .; then + echo "**TEST FAILURE:** The PR branch should never have been pushed on the early-out path." >> "$GITHUB_STEP_SUMMARY" + exit=1 fi + exit $exit + - uses: ./test/update-config/.github/actions/test-teardown if: ${{ always() }} with: branch_prefix: "${{ github.job }}-" test-identical-config: -# if: false name: Test updating the mirrored config branch only to find it matches what's already in staging. runs-on: ubuntu-latest env: @@ -321,7 +749,7 @@ jobs: test_site_repo_live_branch: "${{ github.job }}-main" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: test/update-config @@ -345,11 +773,10 @@ jobs: git remote add remote-config ../remote git fetch remote-config git checkout -b ${{ env.test_site_repo_config_branch }} remote-config/${{ env.test_config_repo_branch }}^ - #tmp_dir=$(mktemp -d) - #cp * "$tmp_dir" - #git push origin HEAD + git push origin HEAD - uses: ./test/update-config + id: run_action with: config_repo: ${{ vars.TEST_CONFIG_REPO }} config_repo_token: ${{ secrets.TEST_CONFIG_REPO_TOKEN }} @@ -377,9 +804,6 @@ jobs: run: | set -eu exit=0 - #tmp_dir=$(mktemp -d) - #git checkout ${{ env.test_site_repo_config_branch }} - #cp * "$tmp_dir" if [[ "$(gh pr list --head ${{ env.test_site_repo_pr_branch }} --json id)" != "[]" ]]; then echo "**TEST FAILURE:** There should not be a PR created." >> "$GITHUB_STEP_SUMMARY" @@ -387,21 +811,30 @@ jobs: fi git checkout ${{ env.test_site_repo_pr_branch_base }} - + if [[ "$(cat config/sync/c)" != "modified" ]]; then echo "**TEST FAILURE:** The PR base branch doesn't contain the updated file 'c'." >> "$GITHUB_STEP_SUMMARY" exit=1 fi - + if [[ "$(cat config/sync/e)" != "modified" ]]; then echo "**TEST FAILURE:** The PR base branch doesn't contain the updated file 'e'." >> "$GITHUB_STEP_SUMMARY" exit=1 fi - - #rm -rf "$tmp_dir" - + exit $exit + - name: Assert the no-op summary names the live branch, not the base branch + env: + SUMMARY: ${{ steps.run_action.outputs.summary }} + run: | + set -eu + printf '%s' "$SUMMARY" | grep -qF "${{ env.test_site_repo_live_branch }}" || { + echo "**TEST FAILURE:** No-op summary should name the live branch (\`${{ env.test_site_repo_live_branch }}\`), was: $SUMMARY" >> "$GITHUB_STEP_SUMMARY"; exit 1; } + if printf '%s' "$SUMMARY" | grep -qF "${{ env.test_site_repo_pr_branch_base }}"; then + echo "**TEST FAILURE:** No-op summary should not name the PR base branch, was: $SUMMARY" >> "$GITHUB_STEP_SUMMARY"; exit 1 + fi + - uses: ./test/update-config/.github/actions/test-teardown if: ${{ always() }} with: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74eed0f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.bats/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3710b41 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +include bats.lock +BATS_DIR := .bats/bats-core-$(BATS_VERSION) +BATS := $(BATS_DIR)/bin/bats +BATS_URL := https://github.com/bats-core/bats-core/archive/refs/tags/v$(BATS_VERSION).tar.gz + +.DEFAULT_GOAL := help +.PHONY: help test clean + +help: + @echo "Available targets:" + @echo " test Run tests/check-and-push-config.bats (downloads BATS on first run)" + @echo " clean Remove any generated/downloaded files" + +$(BATS): + mkdir -p .bats + curl -fsSL $(BATS_URL) -o .bats/bats-$(BATS_VERSION).tar.gz + echo "$(BATS_SHA256) .bats/bats-$(BATS_VERSION).tar.gz" | sha256sum -c + tar -xzf .bats/bats-$(BATS_VERSION).tar.gz -C .bats/ + rm .bats/bats-$(BATS_VERSION).tar.gz + +test: bats.lock $(BATS) + $(BATS) tests/ + +clean: + rm -rf .bats/ diff --git a/README.md b/README.md index aca52fd..33e3750 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Automatic configuration export -![Current test results](https://github.com/andriokha/update-config/actions/workflows/test.yml/badge.svg) - -**TODO: Extract the Drupal module `config_change_track` from Subscriptions.** +![Current test results](https://github.com/FabbDev/update-config/actions/workflows/test.yml/badge.svg) This project supports automatically exporting configuration from a Drupal site and creating a PR with the changes. @@ -36,6 +34,8 @@ and creating a PR with the changes. cp "$config_dir"/* "$config_repo_dir" pushd "$config_repo_dir" git init + git add . + git commit -m "Initial commit" git remote add origin "$config_repo_url" git push origin HEAD popd @@ -46,13 +46,14 @@ and creating a PR with the changes. 2. A read token for the Drupal site repo to pull config changes. 3. Set up the host to push to the config repo: 1. Add the required environment variables, see [`check-and-push-config.sh`]. - 2. Add _Config Change Track_ to the codebase and enable. **TODO: This needs - extracting from Faith Subscriptions.** + 2. Add [Config Change Track] to the codebase and enable. 3. Schedule [`check-and-push-config.sh`] to run regularly. 4. Set up the Drupal site repo to pull from the config repo: - 1. Check [`update-config-branch.yml`] for required permissions, secrets and + 1. In the site repo, go to _Settings → Actions → General_ and enable + **Allow GitHub Actions to create and approve pull requests**. + 2. Check [`update-config-branch.yml`] for required permissions, secrets and variables to set up. - 2. Add [`update-config-branch.yml`] to the Drupal site repo's `/.github` + 3. Add [`update-config-branch.yml`] to the Drupal site repo's `/.github` directory. It's configured to check for changes every 30 minutes (though GitHub doesn't guarantee it will run that frequently). It will create a branch `config-only` that mirrors the `main` branch of the config repo and @@ -66,7 +67,7 @@ with the config repo, and opening a PR when the latest config doesn't match what's in the site repo's staging branch. ```yaml -uses: andriokha/update-config@main +uses: FabbDev/update-config@main with: # The GitHub config repo, eg. MyOrg/MySiteConfig. config_repo: '' @@ -117,5 +118,12 @@ with: github_notify: '' ``` +## Testing the host script + +[`check-and-push-config.sh`] has a self-contained BATS test suite. Run it with +`make test` (BATS is downloaded automatically on first run). Currently the other +tests can only be run on GitHub. + [`check-and-push-config.sh`]: scripts/check-and-push-config.sh [`update-config-branch.yml`]: workflow-templates/update-config-branch.yml +[Config Change Track]: https://www.drupal.org/project/config_change_track diff --git a/action.yml b/action.yml index 5404839..6d32632 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,6 @@ name: Update config branch description: Updates the config-only branch and creates a PR if there are any changes. -# This works as part of automating config export. A cron job on Platform.sh +# This works as part of automating config export. A cron job on the web site # periodically updates an intermediate config repo with the latest config. The # config repo only contains config, it doesn't share a history with the site # repo. This workflow keeps a branch on the site repo up-to-date with a branch @@ -69,13 +69,18 @@ inputs: description: A space-separated list of github users including '@' to ping on a new PR, eg. '@andy @becca'. type: string +outputs: + summary: + description: The primary outcome message of the config update / PR step. + value: ${{ steps.create_pr.outputs.summary }} + runs: using: composite steps: # Optimize for the normal case that the two branches are the same. So start # by doing a shallow checkout of both and checking their hashes. If they're # different, then do a full checkout to enable a merge. - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 id: checkout_site_repo_config_branch with: repository: ${{ inputs.site_repo }} @@ -85,14 +90,14 @@ runs: # It might error if the config-only branch doesn't exist yet. continue-on-error: true - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: ${{ steps.checkout_site_repo_config_branch.outcome == 'failure' }} with: repository: ${{ inputs.site_repo }} token: ${{ inputs.site_repo_token }} path: local - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: ${{ inputs.config_repo }} ref: ${{ inputs.config_repo_branch }} @@ -118,6 +123,8 @@ runs: - name: Read HEAD from new branches shell: sh + env: + INITIAL_SETUP: ${{ steps.checkout_site_repo_config_branch.outcome == 'failure' }} run: | # Read HEAD from new branches set -eu @@ -125,12 +132,12 @@ runs: remote_head=$(git -C remote rev-parse HEAD) echo "local_head=$local_head" >> "$GITHUB_ENV" echo "remote_head=$remote_head" >> "$GITHUB_ENV" - if [ "$local_head" = "$remote_head" ]; then + if [ "$local_head" = "$remote_head" ] && [ "$INITIAL_SETUP" != "true" ]; then echo 'The local config branch `${{ inputs.site_repo_config_branch }}` is up-to-date with the remote. No further action required.' >> $GITHUB_STEP_SUMMARY fi - - uses: actions/checkout@v4 - if: ${{ env.local_head != env.remote_head }} + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + if: ${{ env.local_head != env.remote_head || steps.checkout_site_repo_config_branch.outcome == 'failure' }} with: repository: ${{ inputs.site_repo }} ref: ${{ inputs.site_repo_config_branch }} @@ -138,7 +145,7 @@ runs: fetch-depth: 0 path: local - - uses: actions/checkout@v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # If the repo config branch was created then we already have a full # checkout. if: ${{ env.local_head != env.remote_head && steps.checkout_site_repo_config_branch.outcome != 'failure' }} @@ -162,7 +169,8 @@ runs: git push origin HEAD - name: Create a site branch with the config and open a PR - if: ${{ env.local_head != env.remote_head }} + id: create_pr + if: ${{ env.local_head != env.remote_head || steps.checkout_site_repo_config_branch.outcome == 'failure' }} working-directory: local env: GH_TOKEN: ${{ inputs.site_repo_token }} @@ -170,25 +178,46 @@ runs: run: | # Create a site branch with the config and open a PR set -eu + # Decide whether to reuse the existing PR branch based on whether an + # open PR for this head->base already exists. A branch left behind by a + # closed/rejected PR must not have new commits stacked on its history. + open_pr=$(gh pr list \ + --head ${{ inputs.site_repo_pr_branch }} \ + --base ${{ inputs.site_repo_pr_branch_base }} \ + --state open --json id --jq 'length') source_branch=${{ inputs.site_repo_pr_branch }} - if ! git show-branch remotes/origin/${{ inputs.site_repo_pr_branch }} &> /dev/null; then - git branch ${{ inputs.site_repo_pr_branch }} origin/${{ inputs.site_repo_live_branch }} - source_branch=${{ inputs.site_repo_pr_branch_base }} + if [[ "$open_pr" == "0" ]]; then + # No open PR: recreate the PR branch fresh from live so any stale or + # rejected history is discarded and it holds only the current diff. + git branch -f ${{ inputs.site_repo_pr_branch }} origin/${{ inputs.site_repo_live_branch }} + source_branch=${{ inputs.site_repo_live_branch }} fi git checkout ${{ inputs.site_repo_pr_branch }} - + cd config/sync - rm * - mv "$GITHUB_WORKSPACE"/remote/* . + # Remove all existing config: files, dotfiles, and the subdirectories + # that config collections/config_split produce without tripping `set -e` + # when config/sync is empty. + find . -mindepth 1 -delete + # Copy in the new config, recursing into subdirectories and skipping the + # remote checkout's own .git. `find` is a no-op (not an error) if the + # config repo is empty. + find "$GITHUB_WORKSPACE/remote" -mindepth 1 -maxdepth 1 ! -name .git \ + -exec cp -a {} . \; git add . if [[ -n "$(git status --porcelain)" ]]; then git config user.name '${{ inputs.committer_name }}' git config user.email '${{ inputs.committer_email }}' git commit -m "Export config from Prod" -m "${{ inputs.config_repo }}:$remote_head" - git push origin HEAD - echo 'Updated the config in branch `${{ inputs.site_repo_config_branch }}`.' >> $GITHUB_STEP_SUMMARY - # Create a PR if there isn't already one. - if [[ "$(gh pr list --head ${{ inputs.site_repo_pr_branch }} --json id)" == "[]" ]]; then + # After `git branch -f` the local PR branch diverges from any stale + # `origin/` left by a closed PR. This only ever targets the + # automation-owned PR branch. + git push --force-with-lease origin HEAD + summary="Updated the config in branch \`${{ inputs.site_repo_pr_branch }}\`." + echo "$summary" >> "$GITHUB_STEP_SUMMARY" + echo "summary=$summary" >> "$GITHUB_OUTPUT" + # Create a PR if there isn't already an open one. + if [[ "$open_pr" == "0" ]]; then pr_url=$(gh pr create \ -B ${{ inputs.site_repo_pr_branch_base }} \ -H ${{ inputs.site_repo_pr_branch }} \ @@ -199,5 +228,7 @@ runs: echo "A PR for the branch already exists. No further action required." >> $GITHUB_STEP_SUMMARY fi else - echo "Branch \`${{ inputs.site_repo_config_branch }}\` has been updated, but the changes match what's already in branch \`$source_branch\`. No further action required." >> $GITHUB_STEP_SUMMARY + summary="Branch \`${{ inputs.site_repo_config_branch }}\` has been updated, but the changes match what's already in branch \`$source_branch\`. No further action required." + echo "$summary" >> "$GITHUB_STEP_SUMMARY" + echo "summary=$summary" >> "$GITHUB_OUTPUT" fi diff --git a/bats.lock b/bats.lock new file mode 100644 index 0000000..90ffa3a --- /dev/null +++ b/bats.lock @@ -0,0 +1,2 @@ +BATS_VERSION := 1.11.0 +BATS_SHA256 := aeff09fdc8b0c88b3087c99de00cf549356d7a2f6a69e3fcec5e0e861d2f9063 diff --git a/tests/check-and-push-config.bats b/tests/check-and-push-config.bats new file mode 100644 index 0000000..57823ed --- /dev/null +++ b/tests/check-and-push-config.bats @@ -0,0 +1,203 @@ +#!/usr/bin/env bats +# +# Tests for scripts/check-and-push-config.sh +# Uses a local bare git repo as CONFIG_REPO_URL and a drush stub on PATH. +# No network access or Drupal instance required. + +SCRIPT="$BATS_TEST_DIRNAME/../scripts/check-and-push-config.sh" + +setup() { + # Hermetic git: prevent host ~/.gitconfig and /etc/gitconfig from leaking + # push.default, init.defaultBranch, or identity into the test. + export HOME="$BATS_TEST_TMPDIR/home" + mkdir -p "$HOME" + export GIT_CONFIG_GLOBAL="$HOME/.gitconfig" + export GIT_CONFIG_SYSTEM="$BATS_TEST_TMPDIR/gitconfig.system" + touch "$GIT_CONFIG_SYSTEM" + git config --global user.name "Test User" + git config --global user.email "test@test.com" + git config --global init.defaultBranch "main" + git config --global push.default "simple" + + # Drush stub: all subcommand invocations appended to DRUSH_CALL_LOG. + # Behaviour controlled per-test via NEEDS_EXPORT and EXPORT_FILES_DIR. + local bin_dir="$BATS_TEST_TMPDIR/bin" + mkdir -p "$bin_dir" + export DRUSH_CALL_LOG="$BATS_TEST_TMPDIR/drush-calls.log" + touch "$DRUSH_CALL_LOG" + cat > "$bin_dir/drush" << 'STUB' +#!/usr/bin/env bash +subcommand="$1"; shift +echo "$subcommand $*" >> "$DRUSH_CALL_LOG" +case "$subcommand" in + config-change-track:needs-export) + echo "${NEEDS_EXPORT:-0}" + ;; + config:export) + dest="" + for arg in "$@"; do + case "$arg" in --destination=*) dest="${arg#--destination=}" ;; esac + done + if [[ -n "${EXPORT_FILES_DIR:-}" && -n "$dest" ]]; then + for f in "$EXPORT_FILES_DIR"/*; do + [[ -f "$f" ]] && cp "$f" "$dest/" + done + fi + ;; +esac +STUB + chmod +x "$bin_dir/drush" + export PATH="$bin_dir:$PATH" + + # Bare config repo seeded with one commit on 'main'. + local bare="$BATS_TEST_TMPDIR/config-repo.git" + git init --bare "$bare" + local seed="$BATS_TEST_TMPDIR/seed" + git clone "$bare" "$seed" 2>/dev/null + echo "initial" > "$seed/initial.yml" + git -C "$seed" add . + git -C "$seed" commit -m "Initial commit" --quiet + git -C "$seed" push origin main --quiet + rm -rf "$seed" + + export CONFIG_REPO_URL="$bare" + export CONFIG_REPO_TEMP_DIR="$BATS_TEST_TMPDIR/config-checkout" + + # Default export: one new file not yet present in the repo. + export EXPORT_FILES_DIR="$BATS_TEST_TMPDIR/export-files" + mkdir -p "$EXPORT_FILES_DIR" + echo "exported" > "$EXPORT_FILES_DIR/config.yml" + + unset CONFIG_REPO_BRANCH UPDATE_CONFIG_GIT_NAME UPDATE_CONFIG_GIT_EMAIL \ + UPDATE_CONFIG_GIT_MESSAGE NEEDS_EXPORT +} + +_remote_commit_count() { git -C "$1" rev-list HEAD --count; } +_remote_head() { git -C "$1" rev-parse "${2:-HEAD}"; } + +# Early out + +@test "exits 0 without exporting or pushing when needs-export returns 0" { + export NEEDS_EXPORT=0 + local initial_head; initial_head=$(_remote_head "$CONFIG_REPO_URL") + + run "$SCRIPT" + + [ "$status" -eq 0 ] + ! grep -q "config:export" "$DRUSH_CALL_LOG" + ! grep -q "set-last-export" "$DRUSH_CALL_LOG" + [[ "$(_remote_head "$CONFIG_REPO_URL")" == "$initial_head" ]] +} + +# Fresh clone + +@test "clones repo and pushes exported config when temp dir is empty" { + export NEEDS_EXPORT=1 + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [ "$(_remote_commit_count "$CONFIG_REPO_URL")" -eq 2 ] + # Exported file present in the pushed commit. + git -C "$CONFIG_REPO_URL" show HEAD:config.yml + grep -q "set-last-export --time" "$DRUSH_CALL_LOG" +} + +# Existing checkout (fetch + reset path) + +@test "fetches and resets a stale checkout before exporting and pushing" { + export NEEDS_EXPORT=1 + + # Pre-clone so the script enters the fetch+reset branch. + git clone "$CONFIG_REPO_URL" "$CONFIG_REPO_TEMP_DIR" --quiet 2>/dev/null + + # Advance the remote by one commit so the local checkout is stale. + local update="$BATS_TEST_TMPDIR/update" + git clone "$CONFIG_REPO_URL" "$update" --quiet 2>/dev/null + echo "upstream" > "$update/upstream.yml" + git -C "$update" add . + git -C "$update" commit -m "Upstream" --quiet + git -C "$update" push origin main --quiet + rm -rf "$update" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + # initial(1) + upstream(2) + export(3) = 3 commits. + [ "$(_remote_commit_count "$CONFIG_REPO_URL")" -eq 3 ] + git -C "$CONFIG_REPO_URL" show HEAD:config.yml + grep -q "set-last-export --time" "$DRUSH_CALL_LOG" +} + +# Export produces no diff + +@test "skips commit and push but still calls set-last-export when export yields no diff" { + export NEEDS_EXPORT=1 + # Export exactly what is already committed: initial.yml with "initial". + echo "initial" > "$EXPORT_FILES_DIR/initial.yml" + rm -f "$EXPORT_FILES_DIR/config.yml" + + local initial_head; initial_head=$(_remote_head "$CONFIG_REPO_URL") + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [[ "$(_remote_head "$CONFIG_REPO_URL")" == "$initial_head" ]] + grep -q "set-last-export --time" "$DRUSH_CALL_LOG" +} + +# Commit metadata - custom values + +@test "uses custom git identity and message when env vars are set" { + export NEEDS_EXPORT=1 + export UPDATE_CONFIG_GIT_NAME="Custom Bot" + export UPDATE_CONFIG_GIT_EMAIL="custom@bot.com" + export UPDATE_CONFIG_GIT_MESSAGE="My custom message" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [[ "$(git -C "$CONFIG_REPO_URL" log -1 --format='%an <%ae>')" \ + == "Custom Bot " ]] + [[ "$(git -C "$CONFIG_REPO_URL" log -1 --format='%s')" \ + == "My custom message" ]] +} + +# Commit metadata - defaults + +@test "uses default git identity and message when env vars are unset" { + export NEEDS_EXPORT=1 + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [[ "$(git -C "$CONFIG_REPO_URL" log -1 --format='%an <%ae>')" \ + == "R2D2 " ]] + [[ "$(git -C "$CONFIG_REPO_URL" log -1 --format='%s')" \ + == "Export config from Prod" ]] +} + +# Custom branch + +@test "clones and pushes to the configured branch when CONFIG_REPO_BRANCH is set" { + local custom_bare="$BATS_TEST_TMPDIR/custom-repo.git" + git init --bare "$custom_bare" + local seed="$BATS_TEST_TMPDIR/custom-seed" + git clone "$custom_bare" "$seed" --quiet 2>/dev/null + git -C "$seed" checkout -b config-branch --quiet + echo "initial" > "$seed/initial.yml" + git -C "$seed" add . + git -C "$seed" commit -m "Initial commit" --quiet + git -C "$seed" push --set-upstream origin config-branch --quiet + rm -rf "$seed" + + export CONFIG_REPO_URL="$custom_bare" + export CONFIG_REPO_BRANCH="config-branch" + export NEEDS_EXPORT=1 + + run "$SCRIPT" + + [ "$status" -eq 0 ] + # config.yml must appear on config-branch in the bare repo. + git -C "$custom_bare" show refs/heads/config-branch:config.yml +} diff --git a/workflow-templates/update-config-branch.yml b/workflow-templates/update-config-branch.yml index 185004f..964b817 100644 --- a/workflow-templates/update-config-branch.yml +++ b/workflow-templates/update-config-branch.yml @@ -34,7 +34,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: andriokha/update-config@main + - uses: FabbDev/update-config@main with: config_repo: ${{ vars.CONFIG_REPO }} config_repo_token: ${{ secrets.CONFIG_REPO_TOKEN }}