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
24 changes: 24 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Keeps the pinned versions from silently ageing.
#
# Everything here is pinned deliberately -- actions to a major, golangci-lint to
# an exact version -- which is the right default and also a way to sit on a
# three-year-old toolchain without noticing. These open a pull request instead,
# so an update is a thing somebody decided rather than a thing that drifted.
#
# Monthly, not weekly: this repo has two direct dependencies and a handful of
# actions, and a pull request nobody reads is worse than no pull request.
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: monthly
commit-message:
prefix: "deps"

- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
commit-message:
prefix: "ci"
86 changes: 74 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,15 @@ on:
# Only master on push. A branch with a PR open is covered by pull_request,
# and listening for both ran every one of them twice.
#
# paths-ignore lists the files nothing here reads. README.md is deliberately
# not among them: the language job runs every one of its code blocks through
# the interpreter, so it is executable and a change to it can fail CI.
# Anchors would say this once, but Actions does not expand them.
# No paths filter on either trigger, deliberately. A workflow that a path
# filter stops from triggering reports no check run at all, and a required
# status check waits for it forever -- so the moment master requires these
# checks, a docs-only pull request becomes unmergeable. A job skipped by an
# `if` does report, as "skipped", which satisfies the requirement. So the
# filtering moved from the trigger into the jobs, below.
push:
branches: [master]
paths-ignore:
- 'docs/**'
- 'CLAUDE.md'
- 'LICENSE'
pull_request:
paths-ignore:
- 'docs/**'
- 'CLAUDE.md'
- 'LICENSE'
workflow_dispatch:

permissions:
Expand All @@ -30,8 +24,58 @@ concurrency:
cancel-in-progress: true

jobs:
# Which of the jobs below are worth running. A change confined to files
# nothing here reads should not pay for six jobs across three platforms, and
# this is where that is decided now that the triggers no longer decide it.
#
# README.md is deliberately not in the list: the language job runs every one
# of its code blocks through the interpreter, so it is executable and a change
# to it can fail CI. Same for examples/.
#
# A push to master always runs everything. It is the check after the merge,
# it is rare, and making it conditional would only add a way to be wrong.
changes:
name: changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
# The API rather than a checkout and a diff: no clone, no fetch-depth to
# get wrong, and no third-party action in the supply chain for something
# this small.
- id: filter
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.pull_request.number }}
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
echo "not a pull request, running everything"
exit 0
fi

files="$(gh api "repos/$GH_REPO/pulls/$NUMBER/files" --paginate --jq '.[].filename')"
echo "changed:"
echo "$files" | sed 's/^/ /'

# Anything outside the ignorable set means run everything. Written as
# "is there a file that is not ignorable" rather than the inverse, so
# an unfamiliar path is treated as code and the jobs run.
if echo "$files" | grep -qvE '^(docs/|CLAUDE\.md$|LICENSE$)'; then
echo "code=true" >> "$GITHUB_OUTPUT"
else
echo "code=false" >> "$GITHUB_OUTPUT"
echo "documentation only, skipping the rest"
fi

# The Go side, on every platform we ship a binary for.
test:
needs: changes
if: needs.changes.outputs.code == 'true'
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
Expand Down Expand Up @@ -73,6 +117,8 @@ jobs:
# service is retired -- its badge now renders the word "retired" -- and its
# own site points at golangci-lint instead.
lint:
needs: changes
if: needs.changes.outputs.code == 'true'
name: lint
runs-on: ubuntu-latest
steps:
Expand All @@ -89,9 +135,23 @@ jobs:
with:
version: v2.13.1

# Known vulnerabilities in the dependencies, and in the toolchain itself.
# Unpinned on purpose, unlike golangci-lint above: the reason to pin a
# linter is that a new check turning a branch red says nothing about this
# repo, and the reverse is true here -- a new finding is exactly a fact
# about this repo that nobody knew yesterday. govulncheck also reports
# only what is actually reachable from this code, so a vulnerability in an
# unused corner of a dependency does not stop the build.
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

# The language side. Ubuntu only: both scripts need bash 4 (mapfile) and GNU
# coreutils (timeout), and the macOS runner ships bash 3.2 with neither.
language:
needs: changes
if: needs.changes.outputs.code == 'true'
name: language
runs-on: ubuntu-latest
steps:
Expand All @@ -117,6 +177,8 @@ jobs:

# Cheap, and both targets have found real bugs. 30s each, as in CLAUDE.md.
fuzz:
needs: changes
if: needs.changes.outputs.code == 'true'
name: fuzz
runs-on: ubuntu-latest
steps:
Expand Down