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
22 changes: 22 additions & 0 deletions .github/workflows/caches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ on: # zizmor: ignore[dangerous-triggers] the sweep deletes caches by ref and nev
permissions: {}

jobs:
stop:
name: a closed pull request stops what still runs for it
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: write
steps:
# Merged or not, nothing a closed pull request still runs is read again: main runs its own.
- name: Every run still going for its last commit
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
gh run list --commit "$HEAD_SHA" --event pull_request --limit 100 \
--json databaseId,status \
--jq '.[] | select(.status != "completed") | .databaseId' |
while read -r run; do
gh run cancel "$run" || echo "::notice::run $run had already ended"
done

sweep:
name: a closed pull request takes its caches with it
if: github.event_name == 'pull_request_target'
Expand Down
104 changes: 102 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,76 @@ env:
RUSTFLAGS: -D warnings

jobs:
# Anything not known to stay out of Rust's way counts as Rust, so a new path runs the whole suite.
changes:
name: what the branch touched
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
rust: ${{ steps.look.outputs.rust }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- id: look
env:
EVENT: ${{ github.event_name }}
BASE: ${{ github.event.pull_request.base.sha }}
run: |
if [ "$EVENT" != "pull_request" ]; then
echo "rust=true" >> "$GITHUB_OUTPUT"
exit 0
fi
aside='^(app/src/|app/(\.gitignore|biome\.json|index\.html|package(-lock)?\.json|stryker\.config\.json|tsconfig(\.node)?\.json|vite\.config\.ts)$|hooks/|packaging/|resources/|(CLA|COMMERCIAL|CONTRIBUTING|PRIVACY|README|SECURITY)\.md$|package(-lock)?\.json$|\.markdownlint-cli2\.jsonc$|\.editorconfig$|\.github/(FUNDING|dependabot)\.yml$|\.github/(not-this-spanish|oversized)\.txt$|\.github/workflows/(bundle|caches|cla|commits|feed|mutants|mutants-sweep|release|rules|update-badge)\.yml$)'
from=$(git merge-base "$BASE" HEAD)
changed=$(git diff --name-only "$from" HEAD)
# The resident embeds the news, so it is Rust's though it lives beside the picker.
first=$( { echo "$changed" | grep -x 'app/src/news.json'; echo "$changed" | grep -vE "$aside"; } \
| grep -v '^$' | head -1 || true)
[ -n "$first" ] && rust=true || rust=false
echo "rust=$rust" >> "$GITHUB_OUTPUT"
echo "the first path Rust has to answer for: ${first:-none}"

test:
name: test / ${{ matrix.os }}
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
env:
# A failing test needs a file and a line, not the whole debug info, which is most of what a build writes.
CARGO_PROFILE_DEV_DEBUG: line-tables-only
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# NTFS and the virus scanner are most of why Windows builds and tests slower; a trusted Dev Drive spares both.
- uses: samypr100/setup-dev-drive@562171fe8df7401fdf582d766cd3b6ab80d5b1a0 # v4.1.0
if: runner.os == 'Windows'
with:
drive-size: 25GB
drive-format: ReFS
drive-type: Dynamic
trusted-dev-drive: true
env-mapping: |
CARGO_TARGET_DIR,{{ DEV_DRIVE }}/target
TEMP,{{ DEV_DRIVE }}/tmp
TMP,{{ DEV_DRIVE }}/tmp
- if: runner.os == 'Windows'
shell: bash
run: mkdir -p "$TEMP" "$CARGO_TARGET_DIR"
- uses: ./.github/actions/toolchain
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
shared-key: workspace
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ runner.os == 'Windows' && format('. -> {0}/target', env.DEV_DRIVE) || '. -> target' }}
- uses: ./.github/actions/sidecar
- uses: taiki-e/install-action@5488ac5a951645ce95062214902ac4b6bc1a1037 # v2.87.32
with:
Expand All @@ -50,6 +103,24 @@ jobs:
- if: runner.os == 'macOS'
run: cargo test --doc --workspace

# A skipped matrix never reports the names it would have had, so the required check is this one.
tests:
name: tests
if: always()
needs: [changes, test]
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Every system passed, or the branch never touched what they test
env:
LOOKED: ${{ needs.changes.result }}
RUST: ${{ needs.changes.outputs.rust }}
RESULT: ${{ needs.test.result }}
run: |
[ "$LOOKED" = "success" ] || { echo "::error::what the branch touched could not be read ($LOOKED)"; exit 1; }
[ "$RESULT" = "success" ] || { [ "$RUST" = "false" ] && [ "$RESULT" = "skipped" ]; } \
|| { echo "::error::the tests ended as $RESULT"; exit 1; }

lint:
name: fmt
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -106,10 +177,19 @@ jobs:
with:
persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2
with:
command: check bans licenses sources
# A warning on a pull request, red on main until the branch the Rules advisories job leaves is merged.
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2
continue-on-error: ${{ github.event_name == 'pull_request' }}
with:
command: check advisories
- name: npm advisories in the picker
continue-on-error: ${{ github.event_name == 'pull_request' }}
working-directory: app
run: npm audit --package-lock-only --audit-level=moderate
- name: npm advisories in the repository tooling
continue-on-error: ${{ github.event_name == 'pull_request' }}
env:
# braces has no patched release; it reaches markdownlint-cli2 through globby, dev tooling only.
KNOWN: GHSA-vfj7-8cjw-p6xm
Expand All @@ -130,6 +210,9 @@ jobs:

coverage:
name: coverage
needs: changes
# A dependency bump changes no line of ours, so it pays for neither coverage nor Sonar.
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
Expand All @@ -138,16 +221,25 @@ jobs:
persist-credentials: false

- uses: ./.github/actions/toolchain
if: needs.changes.outputs.rust == 'true'
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
if: needs.changes.outputs.rust == 'true'
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: taiki-e/install-action@5488ac5a951645ce95062214902ac4b6bc1a1037 # v2.87.32
if: needs.changes.outputs.rust == 'true'
with:
tool: cargo-llvm-cov,nextest

# Sonar measures a pull request on its new lines only, and a branch that never touched Rust has none there.
- name: No Rust lines to measure on this branch
if: needs.changes.outputs.rust != 'true'
run: touch lcov-rust.info

- name: What a desktop is not needed to build
if: needs.changes.outputs.rust == 'true'
run: |
# The picker pulls in a tray and a window: tray-icon wants GTK and winit wants the
# display libraries. All of it has to link; none of it has to run, because the tests
Expand Down Expand Up @@ -184,8 +276,9 @@ jobs:

coverage-mac:
name: coverage / the Mac crate
needs: changes
runs-on: macos-latest
if: github.actor != 'dependabot[bot]'
if: github.actor != 'dependabot[bot]' && needs.changes.outputs.rust == 'true'
timeout-minutes: 40
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -215,8 +308,12 @@ jobs:
needs: [coverage, coverage-mac]
runs-on: ubuntu-24.04
timeout-minutes: 15
# The Mac crate is skipped on a branch that never touched Rust, and Sonar still reads the window.
if: >-
github.actor != 'dependabot[bot]'
!cancelled()
&& needs.coverage.result == 'success'
&& (needs.coverage-mac.result == 'success' || needs.coverage-mac.result == 'skipped')
&& github.actor != 'dependabot[bot]'
&& (github.event_name == 'push'
|| github.event.pull_request.head.repo.full_name == github.repository)
steps:
Expand All @@ -231,9 +328,12 @@ jobs:
path: .

- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
if: needs.coverage-mac.result == 'success'
with:
name: coverage-mac
path: .
- if: needs.coverage-mac.result != 'success'
run: touch lcov-mac.info

- uses: SonarSource/sonarqube-scan-action@d209202bc7d53ff1cc128f7f907dac145c9d6ae9 # v8.3.0
env:
Expand Down
110 changes: 108 additions & 2 deletions .github/workflows/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,30 @@ jobs:
- uses: taiki-e/install-action@5488ac5a951645ce95062214902ac4b6bc1a1037 # v2.87.32
with:
tool: zizmor@1.30.1
# Offline, so a pull request is judged on its own lines and never on whether GitHub's API answered.
- name: The workflows mean what they say
env:
GH_TOKEN: ${{ github.token }}
SHELLCHECK_OPTS: --severity=warning
run: |
go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12
"$(go env GOPATH)/bin/actionlint"
zizmor --min-severity medium .github
zizmor --offline --min-severity medium .github

- name: No action pinned here has a known vulnerability
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -uo pipefail
# zizmor exits 1 only when it could not finish; findings exit 10 or more and are not retried.
for try in 1 2 3; do
code=0
zizmor --min-severity medium .github || code=$?
[ "$code" -ne 1 ] && exit "$code"
echo "::warning::try $try did not finish; asking GitHub again in a minute"
sleep 60
done
exit 1

# One implementation for the conventions a person can break in a second: the hook runs
# the same file, so local and CI can never drift into saying different things.
Expand Down Expand Up @@ -99,6 +115,96 @@ jobs:
cargo test -p linkunbound-core --quiet || exit 1
done

# An advisory lands any day; this moves only the crates it names, so a branch is ready before a red main is news.
advisories:
name: The crates an advisory names move past it
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: write
env:
BRANCH: fix/advisories
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
persist-credentials: false

- uses: ./.github/actions/toolchain
- uses: taiki-e/install-action@5488ac5a951645ce95062214902ac4b6bc1a1037 # v2.87.32
with:
tool: cargo-deny

- name: Read the lock and move what it names
id: move
run: |
set -uo pipefail
if cargo deny --format json check advisories 2> "$RUNNER_TEMP/deny.json"; then
echo "Nothing in the lock has an advisory or a yank against it."
exit 0
fi
mapfile -t named < <(jq -r 'select(.type == "diagnostic" and .fields.severity == "error")
| .fields.graphs[]?.Krate | "\(.name)@\(.version)"' "$RUNNER_TEMP/deny.json" | sort -u)
if [ "${#named[@]}" -eq 0 ]; then
cat "$RUNNER_TEMP/deny.json"
echo "::error::cargo-deny failed without naming a crate; read the log above."
exit 1
fi
for one in "${named[@]}"; do
cargo update -p "$one" || echo "::warning::$one has no compatible release to move to"
done
if git diff --quiet Cargo.lock; then
echo "::error::no compatible release moves past ${named[*]}: it needs a hand, or an entry in deny.toml."
exit 1
fi
if ! cargo deny check advisories; then
echo "::error::the lock moved but still fails; the branch was not pushed."
exit 1
fi
crates=$(printf '%s\n' "${named[@]}" | sed 's/@.*//' | sort -u | paste -sd, - | sed 's/,/, /g')
echo "crates=$crates" >> "$GITHUB_OUTPUT"

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: steps.move.outputs.crates != ''
with:
node-version: 22
package-manager-cache: false

# The notices job refuses a lock the notices do not match, so they move with it.
- name: The notices the new lock carries
if: steps.move.outputs.crates != ''
run: |
npm ci --prefix app
npm run notices

- name: Leave the branch ready
if: steps.move.outputs.crates != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CRATES: ${{ steps.move.outputs.crates }}
run: |
set -euo pipefail
remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
# A commit someone added to the open branch is work; forcing today's lock over it would throw it away.
if git ls-remote --exit-code --heads "$remote" "$BRANCH" > /dev/null; then
git fetch -q "$remote" "$BRANCH"
if git log --no-merges --format=%an HEAD..FETCH_HEAD | grep -vqx 'github-actions\[bot\]'; then
echo "::error::$BRANCH carries commits by hand, so it was left as it is; merge or close it: https://github.com/${GITHUB_REPOSITORY}/compare/main...${BRANCH}"
exit 1
fi
fi
subject="fix(deps): $CRATES past today's advisories"
[ "$(printf '%s' "$subject" | wc -m)" -le 110 ] || subject="fix(deps): the crates past today's advisories"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -c "$BRANCH"
git commit -qm "$subject" Cargo.lock THIRD-PARTY-BUNDLED.md THIRD-PARTY-LICENSES.md
git push -f "$remote" "$BRANCH"
# Red on purpose: main fails the same check until this is merged, and a red run is what reaches the inbox.
echo "::error::$subject — open it: https://github.com/${GITHUB_REPOSITORY}/compare/main...${BRANCH}?expand=1"
exit 1

# The release refuses notices behind the lockfiles; this says so on the pull request that moves them, not on the tag.
notices:
name: The notices name what ships
Expand Down
Loading
Loading