From eef23b4174eeb865715d238325525de91727fc10 Mon Sep 17 00:00:00 2001 From: AndyF Date: Fri, 26 Jun 2026 11:38:25 +0100 Subject: [PATCH 01/15] chore: move action from andriokha to FabbDev --- README.md | 4 ++-- workflow-templates/update-config-branch.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aca52fd..d2fc9ea 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Automatic configuration export -![Current test results](https://github.com/andriokha/update-config/actions/workflows/test.yml/badge.svg) +![Current test results](https://github.com/FabbDev/update-config/actions/workflows/test.yml/badge.svg) **TODO: Extract the Drupal module `config_change_track` from Subscriptions.** @@ -66,7 +66,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: '' 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 }} From edb6a8d06b262ca0e8f792161344ab7f3e305ded Mon Sep 17 00:00:00 2001 From: AndyF Date: Fri, 26 Jun 2026 11:40:03 +0100 Subject: [PATCH 02/15] docs: add some more detail --- .github/workflows/test.yml | 6 ++++-- README.md | 14 ++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0065305..5c7fbad 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. diff --git a/README.md b/README.md index d2fc9ea..e58d5c2 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ ![Current test results](https://github.com/FabbDev/update-config/actions/workflows/test.yml/badge.svg) -**TODO: Extract the Drupal module `config_change_track` from Subscriptions.** - 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 @@ -119,3 +120,4 @@ with: [`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 From fc9f14d6bf78354f6558a31ff36a7cd2edfe87f3 Mon Sep 17 00:00:00 2001 From: AndyF Date: Mon, 29 Jun 2026 16:17:08 +0100 Subject: [PATCH 03/15] test: use correct shell in test-assertions.yml --- .github/actions/test-assertions/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 62fdb842c7f247833afbe3a71d7be9e951f84642 Mon Sep 17 00:00:00 2001 From: AndyF Date: Mon, 29 Jun 2026 16:46:50 +0100 Subject: [PATCH 04/15] test: fix test-identical-config --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c7fbad..c59093a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -347,9 +347,7 @@ 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 with: From 62b9e45544e4c9fa61485f86c96d26ab021fc77f Mon Sep 17 00:00:00 2001 From: AndyF Date: Mon, 29 Jun 2026 18:56:46 +0100 Subject: [PATCH 05/15] test: expand test-existing-pr --- .github/workflows/test.yml | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c59093a..e7df49d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -299,13 +299,40 @@ 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 + + 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 + - uses: ./test/update-config/.github/actions/test-teardown if: ${{ always() }} with: From bd04e8def4cbc2024e90880418600e540ad8dffb Mon Sep 17 00:00:00 2001 From: AndyF Date: Mon, 29 Jun 2026 19:09:13 +0100 Subject: [PATCH 06/15] test: add a test for the steady-state no-op That's when the config mirror matches the current config and there's nothing to do. --- .github/workflows/test.yml | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7df49d..a9a8f8c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -338,6 +338,89 @@ jobs: 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@v4 + 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. From 13f17e8d4d3cc7d8cd5801fc74082a4b4db6036a Mon Sep 17 00:00:00 2001 From: AndyF Date: Mon, 29 Jun 2026 19:49:43 +0100 Subject: [PATCH 07/15] test: improve coverage --- .github/actions/test-setup/action.yml | 2 +- .github/workflows/test.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/test-setup/action.yml b/.github/actions/test-setup/action.yml index 866f3da..911e731 100644 --- a/.github/actions/test-setup/action.yml +++ b/.github/actions/test-setup/action.yml @@ -73,7 +73,7 @@ runs: git add . git commit -m "Add initial mock config" echo modified > c - touch e + echo baseline > e git add c e git rm d git commit -m "Updated" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a9a8f8c..b05e256 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -146,6 +146,7 @@ jobs: git checkout ${{ env.test_site_repo_live_branch }} rm config/sync/* mv "$tmp_dir"/* config/sync + echo baseline > config/sync/e git add config/sync git commit --amend -m "Update" git push --force origin HEAD From 24e815d420f665fed451c3dec83ff1681be7641f Mon Sep 17 00:00:00 2001 From: AndyF Date: Tue, 30 Jun 2026 11:59:33 +0100 Subject: [PATCH 08/15] test: expand test-create-pr --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b05e256..e46d027 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -194,7 +194,11 @@ jobs: 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 From d20880eb62a16a0dacebe322963d9ca8c6bc1c93 Mon Sep 17 00:00:00 2001 From: AndyF Date: Tue, 30 Jun 2026 13:09:16 +0100 Subject: [PATCH 09/15] fix: correct and test message output --- .github/workflows/test.yml | 55 +++++++++++++++++++++++++++++--------- action.yml | 16 ++++++++--- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e46d027..83b0396 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -160,6 +160,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 }} @@ -187,12 +188,12 @@ 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 }} 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" @@ -203,14 +204,25 @@ jobs: 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 - + 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: @@ -279,6 +291,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 }} @@ -338,6 +351,17 @@ jobs: 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: @@ -465,6 +489,7 @@ jobs: 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 }} @@ -492,9 +517,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" @@ -502,21 +524,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/action.yml b/action.yml index 5404839..4b08592 100644 --- a/action.yml +++ b/action.yml @@ -69,6 +69,11 @@ 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: @@ -162,6 +167,7 @@ runs: git push origin HEAD - name: Create a site branch with the config and open a PR + id: create_pr if: ${{ env.local_head != env.remote_head }} working-directory: local env: @@ -173,7 +179,7 @@ runs: 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 }} + source_branch=${{ inputs.site_repo_live_branch }} fi git checkout ${{ inputs.site_repo_pr_branch }} @@ -186,7 +192,9 @@ runs: 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 + 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 one. if [[ "$(gh pr list --head ${{ inputs.site_repo_pr_branch }} --json id)" == "[]" ]]; then pr_url=$(gh pr create \ @@ -199,5 +207,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 From 354bae4ad8bb5e283b3e071b26dcea62f52de507 Mon Sep 17 00:00:00 2001 From: AndyF Date: Tue, 30 Jun 2026 15:13:08 +0100 Subject: [PATCH 10/15] test: test check-and-push-config.sh --- .github/workflows/test-script.yml | 26 ++++ .gitignore | 1 + Makefile | 25 ++++ README.md | 6 + bats.lock | 2 + tests/check-and-push-config.bats | 203 ++++++++++++++++++++++++++++++ 6 files changed, 263 insertions(+) create mode 100644 .github/workflows/test-script.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 bats.lock create mode 100644 tests/check-and-push-config.bats diff --git a/.github/workflows/test-script.yml b/.github/workflows/test-script.yml new file mode 100644 index 0000000..1732377 --- /dev/null +++ b/.github/workflows/test-script.yml @@ -0,0 +1,26 @@ +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@v4 + + - name: Cache BATS + uses: actions/cache@v4 + with: + path: .bats + key: bats-${{ hashFiles('bats.lock') }} + + - run: make test 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 e58d5c2..33e3750 100644 --- a/README.md +++ b/README.md @@ -118,6 +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/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 +} From 44c06bd6f340d74ee08095e7ffa04a05f1f58c83 Mon Sep 17 00:00:00 2001 From: AndyF Date: Wed, 1 Jul 2026 17:33:38 +0100 Subject: [PATCH 11/15] chore: clean up comments --- .github/workflows/test.yml | 4 ---- action.yml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83b0396..52ba815 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,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: @@ -101,7 +100,6 @@ jobs: 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: @@ -229,7 +227,6 @@ jobs: branch_prefix: "${{ github.job }}-" test-existing-pr: -# if: false name: Test updating an existing PR. runs-on: ubuntu-latest env: @@ -451,7 +448,6 @@ jobs: 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: diff --git a/action.yml b/action.yml index 4b08592..202903d 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 From c6e0f134273a4668a3bd979693e45292bb82cf1b Mon Sep 17 00:00:00 2001 From: AndyF Date: Wed, 1 Jul 2026 17:37:04 +0100 Subject: [PATCH 12/15] fix: create a pr on first run Previously if you initially set this up on a project at a time when the config's already been changed, that wouldn't cause a PR to be created: you'd have to wait until the next config change. --- .github/workflows/test.yml | 102 +++++++++++++++++++++++++++++++++++++ action.yml | 8 +-- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52ba815..4894a59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -99,6 +99,108 @@ 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@v4 + 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 }} + + - 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 + + 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: name: Test updating the mirrored config branch and creating a PR from it. runs-on: ubuntu-latest diff --git a/action.yml b/action.yml index 202903d..e935275 100644 --- a/action.yml +++ b/action.yml @@ -123,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 @@ -130,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 }} + 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 }} @@ -168,7 +170,7 @@ runs: - name: Create a site branch with the config and open a PR id: create_pr - if: ${{ env.local_head != env.remote_head }} + 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 }} From 2c3f2c58752b1847131d3860fc9951652a2e1c7a Mon Sep 17 00:00:00 2001 From: AndyF Date: Wed, 1 Jul 2026 18:07:31 +0100 Subject: [PATCH 13/15] fix: don't update a closed PR Previously if the config-only branch existed then it was used, even if it was connected with a rejected PR. Not it only adds to the existing branch if there's an open PR associated with it. --- .github/workflows/test.yml | 141 +++++++++++++++++++++++++++++++++++++ action.yml | 24 +++++-- 2 files changed, 159 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4894a59..2d80ca3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,6 +328,147 @@ jobs: 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@v4 + 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 }} + rm config/sync/* + 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 + if: ${{ always() }} + with: + branch_prefix: "${{ github.job }}-" + test-existing-pr: name: Test updating an existing PR. runs-on: ubuntu-latest diff --git a/action.yml b/action.yml index e935275..e13e788 100644 --- a/action.yml +++ b/action.yml @@ -178,13 +178,22 @@ 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 }} + 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/* . @@ -193,12 +202,15 @@ runs: 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 + # 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 one. - if [[ "$(gh pr list --head ${{ inputs.site_repo_pr_branch }} --json id)" == "[]" ]]; then + # 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 }} \ From 63229d3115f2f848fe7680f8024cf2be5bb42994 Mon Sep 17 00:00:00 2001 From: AndyF Date: Wed, 1 Jul 2026 20:18:32 +0100 Subject: [PATCH 14/15] chore: update action dependencies actions/cache -> 6.0.0 actions/checkout -> 7.0.0 --- .github/actions/test-checkout/action.yml | 4 ++-- .github/workflows/test-script.yml | 5 ++--- .github/workflows/test.yml | 14 +++++++------- action.yml | 10 +++++----- 4 files changed, 16 insertions(+), 17 deletions(-) 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/workflows/test-script.yml b/.github/workflows/test-script.yml index 1732377..75842f0 100644 --- a/.github/workflows/test-script.yml +++ b/.github/workflows/test-script.yml @@ -15,10 +15,9 @@ jobs: test-script: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Cache BATS - uses: actions/cache@v4 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: .bats key: bats-${{ hashFiles('bats.lock') }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d80ca3..97721f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,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 @@ -112,7 +112,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 @@ -212,7 +212,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 @@ -339,7 +339,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 @@ -480,7 +480,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 @@ -618,7 +618,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 @@ -701,7 +701,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 diff --git a/action.yml b/action.yml index e13e788..881c99c 100644 --- a/action.yml +++ b/action.yml @@ -80,7 +80,7 @@ runs: # 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 }} @@ -90,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 }} @@ -136,7 +136,7 @@ runs: 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 + - 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 }} @@ -145,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' }} From 5a3e75ab15943cd3901565371a9c95f422273790 Mon Sep 17 00:00:00 2001 From: AndyF Date: Thu, 2 Jul 2026 13:33:05 +0100 Subject: [PATCH 15/15] feat: support config subdirectories --- .github/actions/test-setup/action.yml | 7 +++- .github/workflows/test.yml | 54 +++++++++++++++++++++++++-- action.yml | 11 +++++- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/.github/actions/test-setup/action.yml b/.github/actions/test-setup/action.yml index 911e731..c24e388 100644 --- a/.github/actions/test-setup/action.yml +++ b/.github/actions/test-setup/action.yml @@ -74,7 +74,10 @@ runs: git commit -m "Add initial mock config" echo modified > c echo baseline > e - git add c 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.yml b/.github/workflows/test.yml index 97721f3..2be892d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -140,6 +140,15 @@ jobs: 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: @@ -183,6 +192,25 @@ jobs: 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 @@ -244,7 +272,7 @@ 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 @@ -310,6 +338,16 @@ jobs: 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 @@ -373,7 +411,7 @@ 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 @@ -514,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" @@ -583,6 +621,16 @@ jobs: 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" diff --git a/action.yml b/action.yml index 881c99c..6d32632 100644 --- a/action.yml +++ b/action.yml @@ -195,8 +195,15 @@ runs: 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 }}'