diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..566a84b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# Weekly, grouped updates; the cooldown leaves a new release a week to be +# pulled if it is compromised. Commits need the maintainer's sign-off before +# merge, like any other (git rebase --signoff). +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + actions: + patterns: ["*"] + commit-message: + prefix: ci diff --git a/.github/scripts/base-commit.sh b/.github/scripts/base-commit.sh deleted file mode 100755 index 351eb37..0000000 --- a/.github/scripts/base-commit.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -# base-commit.sh -- print the commit this workflow run's changes are measured -# from, or nothing when there is none (scheduled and manual runs). -# -# pull_request the base branch commit -# push the previous tip of the branch; after a force push or on a -# new branch, the parent of the first commit the push brought -# -# Needs jq, the GitHub event payload and a checkout deep enough to hold the -# base (fetch-depth: 100, like pmaports' own CI). -set -eu -event=$GITHUB_EVENT_PATH -case "$GITHUB_EVENT_NAME" in -pull_request) - jq -r .pull_request.base.sha "$event" - ;; -push) - before=$(jq -r .before "$event") - if git merge-base --is-ancestor "$before" HEAD 2>/dev/null; then - echo "$before" - else - first=$(jq -r '.commits[0].id // empty' "$event") - # A root commit (the first push to a new repository) has no parent: - # print nothing, so there is no range to check. - if [ -n "$first" ]; then git rev-parse -q --verify "$first^" || true; fi - fi - ;; -esac diff --git a/.github/scripts/commit-check.sh b/.github/scripts/commit-check.sh deleted file mode 100755 index 121f611..0000000 --- a/.github/scripts/commit-check.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh -# commit-check.sh BASE HEAD [--dco] -- check the trailers of the commits in -# BASE..HEAD. -# -# Rejected in every commit, whoever wrote it: -# - an AI tool named in Co-authored-by: or Co-developed-by: -# - an AI tool or bot named in Signed-off-by: (a sign-off is a person's -# Developer Certificate of Origin) -# - Claude-Session: trailers, AI session URLs, "Generated with [...]" lines -# - a malformed Assisted-by: or Generated-by: trailer. Both are optional; -# they are expected when an AI tool was involved (see CONTRIBUTING.md). -# With --dco (pull requests), every commit also needs a Signed-off-by: -# matching its author. -set -eu -base=$1 head=$2 dco=${3:-} -ai='claude|anthropic|openai|chatgpt|copilot|gemini|codex|cursor agent|\[bot\]' -wrong="^(co-authored-by|co-developed-by):.*($ai)|^signed-off-by:.*($ai)|^claude-session:|claude\.ai/code/session_|chatgpt\.com/(share|c)/|^.{0,8}generated with \[" -fail=0 -n=0 -for c in $(git rev-list "$base..$head"); do - n=$((n + 1)) - short=$(git rev-parse --short "$c") - msg=$(git log -1 --format=%B "$c") - if printf '%s\n' "$msg" | grep -qiE "$wrong"; then - echo "::error::$short: AI attribution in a trailer or line where it does not belong" - fail=1 - fi - if printf '%s\n' "$msg" | grep -iE '^(assisted|generated)-by:' | grep -qvE '^(Assisted|Generated)-by: [^[:space:]]'; then - echo "::error::$short: malformed Assisted-by: or Generated-by: trailer (expected 'Assisted-by: ')" - fail=1 - fi - if [ "$dco" = --dco ]; then - author=$(git log -1 --format='%an <%ae>' "$c") - if ! git log -1 --format='%(trailers:key=Signed-off-by,valueonly)' "$c" | grep -qxF "$author"; then - echo "::error::$short: no Signed-off-by: matching the commit author (git commit -s, or git rebase --signoff)" - fail=1 - fi - fi -done -echo "checked $n commit(s)" -exit "$fail" diff --git a/.github/scripts/test-commit-check.sh b/.github/scripts/test-commit-check.sh deleted file mode 100755 index f1c9519..0000000 --- a/.github/scripts/test-commit-check.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/sh -# test-commit-check.sh -- run commit-check.sh against throwaway commits. -set -eu -check=$(cd "$(dirname "$0")" && pwd)/commit-check.sh -repo=$(mktemp -d) -trap 'rm -rf "$repo"' EXIT -cd "$repo" -git init -q -git config user.name "Jane Doe" -git config user.email jane@example.org -git commit -q --allow-empty -m base - -# expect pass|fail, extra check flags, commit message -expect() { - result=$1 flags=$2 - git commit -q --allow-empty -m "$3" - # $flags is empty or one word: unquoted so an empty one passes nothing. - # shellcheck disable=SC2086 - if sh "$check" HEAD~1 HEAD $flags >/dev/null 2>&1; then got=pass; else got=fail; fi - if [ "$got" != "$result" ]; then - echo "FAIL: expected $result, got $got for: $3" >&2 - exit 1 - fi -} - -sob="Signed-off-by: Jane Doe " -expect pass "" "human change, no trailers" -expect pass "" "change - -Assisted-by: Claude" -expect pass "" "change - -Generated-by: some-tool 1.0" -expect pass --dco "change - -Assisted-by: Claude -$sob" -expect pass --dco "human change - -$sob" -expect fail --dco "change without sign-off" -expect fail --dco "change - -Signed-off-by: Someone Else " -expect fail "" "change - -Co-authored-by: Claude " -expect fail "" "change - -Co-developed-by: GitHub Copilot" -expect fail "" "change - -Signed-off-by: Claude " -expect fail "" "change - -Signed-off-by: renovate[bot] " -expect fail "" "change - -Claude-Session: 0123" -expect fail "" "change - -🤖 Generated with [Claude Code](https://claude.com/claude-code)" -expect fail "" "change - -Assisted-by:" -expect fail "" "change - -assisted-by: claude" -echo "commit-check: all cases pass" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0be767..e6ad104 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,22 +1,26 @@ -# Check the signing keys, the documentation links and the commit trailers. +# Check the signing keys, the documentation links and the commits on pushes to +# main, pull requests and merge groups. name: CI on: push: branches: [main] pull_request: + merge_group: permissions: contents: read concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # Superseded pull request runs are cancelled; every push to main keeps its result. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: lint: name: Lint runs-on: ubuntu-24.04 + timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -50,17 +54,27 @@ jobs: commit-check: name: Commit check runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: porthole-dev/.github/actions/commit-check@06b66d018629f11ebf2e305940855793dbc0b775 # v1.0.0 + + workflow-lint: + name: Workflow lint + runs-on: ubuntu-24.04 + timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 100 - ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Test the check - run: sh .github/scripts/test-commit-check.sh - - name: Check trailers and DCO sign-off - run: | - base=$(sh .github/scripts/base-commit.sh) - if [ -z "$base" ]; then echo "no new commits"; exit 0; fi - sh .github/scripts/commit-check.sh "$base" HEAD \ - ${{ github.event_name == 'pull_request' && '--dco' || '' }} + - uses: porthole-dev/.github/actions/workflow-lint@06b66d018629f11ebf2e305940855793dbc0b775 # v1.0.0 + + ci-passed: + name: CI passed + if: ${{ !cancelled() }} + needs: [lint, commit-check, workflow-lint] + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: porthole-dev/.github/actions/ci-passed@06b66d018629f11ebf2e305940855793dbc0b775 # v1.0.0 + with: + results: ${{ join(needs.*.result, ' ') }}