Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,35 @@ jobs:
with:
files: coverage/coverage.xml
fail_ci_if_error: false

# Merges a Dependabot pull request that changes only the CI image, after
# lint and test passed. Off unless the repository variable
# CI_IMAGE_AUTO_MERGE is true.
merge-image:
needs: [lint, test]
if: >-
vars.CI_IMAGE_AUTO_MERGE == 'true' &&
github.event_name == 'pull_request' &&
github.actor == 'dependabot[bot]' &&
github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
pull-requests: write

steps:
- name: Merge image update
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
files=$(gh api "repos/$REPO/pulls/$PR/files" --paginate --jq '.[].filename')
if [[ "$files" != ".github/ci-image/Dockerfile" ]]; then
echo "::notice::#$PR changes more than .github/ci-image/Dockerfile and stays open"
exit 0
fi
# Merges nothing when the branch moved after lint and test
gh pr merge "$PR" -R "$REPO" --squash --match-head-commit "$HEAD_SHA"
1 change: 1 addition & 0 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ sudo pacman -S --needed git openssh go-yq curl pacman-contrib namcap github-cli
### Recommended

- **Enable Dependabot version updates**: Settings > Advanced Security, then Enable next to "Dependabot version updates". GitHub leaves them off on forks. Dependabot then opens pull requests for new action versions and for the digest of the Arch image the workflows run in.
- **Merge Arch image updates automatically** (optional): Settings > Secrets and variables > Actions > Variables > New repository variable, with the name `CI_IMAGE_AUTO_MERGE` and the value `true`. A new image then reaches the jobs with the secrets after the tests and without a review, see [WORKFLOW.md](WORKFLOW.md#63-ci-image-updates).
- Set repository topics if your fork is public.

### Badges
Expand Down
10 changes: 9 additions & 1 deletion docs/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ The workflow runs every hour (`cron: "0 * * * *"`) and by hand through `workflow

### 5.2 Permissions and container

The workflow has three jobs after a short `image` job. `image` reads the image from `.github/ci-image/Dockerfile`: `archlinux:base-devel`, pinned by digest, so a new image on Docker Hub reaches the jobs with the secrets only through a reviewed commit. Dependabot updates the digest once a month with a pull request, and `ci.yml` runs the tests in the new image before the merge. The three jobs run in that image on `ubuntu-latest` with a 30-minute timeout, and `vercmp` from pacman, which `check-all.sh` requires, is the reason `verify` and `publish` need Arch too. Every install step upgrades `archlinux-keyring` before the system, because a pinned image ages and an old keyring rejects packages signed with a newer packager key. `makepkg` refuses to run as root, so the steps that touch packages in `prepare` and `publish` run as a `builder` user. `builder` has no `sudo` rights: the steps run as root and switch to `builder` with `sudo -u builder`. `verify` executes no PKGBUILD and runs as root.
The workflow has three jobs after a short `image` job. `image` reads the image from `.github/ci-image/Dockerfile`: `archlinux:base-devel`, pinned by digest, so a new image on Docker Hub reaches the jobs with the secrets only through a reviewed commit. Dependabot updates the digest once a month with a pull request, and `ci.yml` runs the tests in the new image before the merge, see [6.3](#63-ci-image-updates). The three jobs run in that image on `ubuntu-latest` with a 30-minute timeout, and `vercmp` from pacman, which `check-all.sh` requires, is the reason `verify` and `publish` need Arch too. Every install step upgrades `archlinux-keyring` before the system, because a pinned image ages and an old keyring rejects packages signed with a newer packager key. `makepkg` refuses to run as root, so the steps that touch packages in `prepare` and `publish` run as a `builder` user. `builder` has no `sudo` rights: the steps run as root and switch to `builder` with `sudo -u builder`. `verify` executes no PKGBUILD and runs as root.

| Job | Permissions | Secrets |
|-----|-------------|---------|
Expand Down Expand Up @@ -785,6 +785,14 @@ bashcov -- bats tests/ --recursive

The coverage run uploads `coverage/coverage.xml` to Codecov and never fails the job.

### 6.3 CI image updates

Once a month, when Docker Hub has a new `archlinux:base-devel`, Dependabot opens a pull request that changes the digest in `.github/ci-image/Dockerfile`. The `image` job reads the digest from the pull request, so `test` runs in the new image before the merge.

The `merge-image` job merges such a pull request without a review. It is off until the repository variable `CI_IMAGE_AUTO_MERGE` is `true`, see [SETUP.md](SETUP.md#recommended). It runs after `lint` and `test` passed, only for a pull request that `dependabot[bot]` opened, on an event Dependabot caused, and gets `contents: write` and `pull-requests: write`. A pull request that changes any other file, a Dependabot update of an action for example, stays open with a notice. Otherwise the job squash-merges it with `--match-head-commit`, so a commit pushed after the tests stops the merge.

With the variable set, a new image reaches the jobs with the secrets after the tests and without a review. A branch rule that requires an approval, or an up-to-date branch after `main` has moved, makes the merge fail, and the pull request waits for a manual merge.

## 7. Local Testing

### 7.1 Prerequisites
Expand Down
60 changes: 60 additions & 0 deletions tests/workflows.bats
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,63 @@ run_blocks() {
[ "$status" -eq 0 ]
done
}

@test "merge-image: runs only after lint and test" {
run yq '.jobs["merge-image"].needs | sort | join(" ")' "$GITHUB_DIR/workflows/ci.yml"
[ "$status" -eq 0 ]
[ "$output" = "lint test" ]
}

@test "merge-image: off unless the variable is true and Dependabot opened the pull request" {
run yq '.jobs["merge-image"].if' "$GITHUB_DIR/workflows/ci.yml"
[ "$status" -eq 0 ]
[[ "$output" == *"vars.CI_IMAGE_AUTO_MERGE == 'true'"* ]]
[[ "$output" == *"github.event_name == 'pull_request'"* ]]
[[ "$output" == *"github.actor == 'dependabot[bot]'"* ]]
[[ "$output" == *"github.event.pull_request.user.login == 'dependabot[bot]'"* ]]
[[ "$output" != *"||"* ]]
}

# Runs the merge step of ci.yml with a gh that prints the files in
# $GH_FILES and logs every call to $GH_LOG
run_merge_step() {
local bin="$BATS_TEST_TMPDIR/bin"
mkdir -p "$bin"
cat >"$bin/gh" <<'EOF'
#!/usr/bin/env bash
echo "$*" >>"$GH_LOG"
if [[ "$1" == "api" ]]; then
[[ -z "${GH_FAIL:-}" ]] || exit 1
printf '%s\n' "$GH_FILES"
fi
EOF
chmod +x "$bin/gh"
yq '.jobs["merge-image"].steps[] | select(.name == "Merge image update") | .run' \
"$GITHUB_DIR/workflows/ci.yml" >"$BATS_TEST_TMPDIR/step.sh"
GH_LOG="$BATS_TEST_TMPDIR/gh.log"
: >"$GH_LOG"
run env PATH="$bin:$PATH" GH_LOG="$GH_LOG" REPO=owner/repo PR=12 HEAD_SHA=abc123 \
bash -e "$BATS_TEST_TMPDIR/step.sh"
}

@test "merge-image: merges the tested head when only the Dockerfile changed" {
GH_FILES=".github/ci-image/Dockerfile" run_merge_step
[ "$status" -eq 0 ]
grep -qx 'api repos/owner/repo/pulls/12/files --paginate --jq .\[\].filename' "$GH_LOG"
grep -qx 'pr merge 12 -R owner/repo --squash --match-head-commit abc123' "$GH_LOG"
}

@test "merge-image: leaves a pull request that changes other files" {
GH_FILES=$'.github/ci-image/Dockerfile\n.github/workflows/ci.yml' run_merge_step
[ "$status" -eq 0 ]
[[ "$output" == *"::notice::"* ]]
run grep -c '^pr merge' "$GH_LOG"
[ "$output" = "0" ]
}

@test "merge-image: fails when the file list cannot be read" {
GH_FAIL=1 GH_FILES=".github/ci-image/Dockerfile" run_merge_step
[ "$status" -ne 0 ]
run grep -c '^pr merge' "$GH_LOG"
[ "$output" = "0" ]
}
Loading