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
27 changes: 26 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
# on every download instead.
#
# After Dependabot bumps a version in Build/Dependencies, .github/workflows/dependency-lock-sync.yml
# refreshes the matching SHA-256 in the lock file. PR validation fails while the two disagree.
# refreshes the matching SHA-256 in the lock file and the version the SBOM inventory in
# Build/Dependencies.psd1 reports. It sweeps the open Dependabot pull requests weekly, so a bump
# arrives already synced. PR validation fails while any of the three disagree.
#
# Note that GitHub reads this file from the default branch only, so an ignore rule added here takes
# effect on Dependabot's next run - it does not retroactively close pull requests that are already
# open. Those have to be closed by hand.
version: 2
updates:
- package-ecosystem: "github-actions"
Expand Down Expand Up @@ -49,6 +55,25 @@ updates:
update-types:
- "version-update:semver-major"

# The rest of the System.Text.Json closure - the Group = "SystemTextJson" artefacts in
# OmadaWeb.PS/DependencyLock.psd1. These are not independently upgradable: the module loads
# them with Assembly.LoadFrom, which applies no binding redirects, so the versions that get
# loaded have to be the ones the pinned System.Text.Json actually resolves. A bump of a single
# member is therefore never a change this module can take on its own - it moves when
# System.Text.Json moves, and Build/Update-DependencyLock.ps1 -Check enforces that these entries
# exist so a closure member can never be added to the lock without landing here too.
#
# Listing them without update-types ignores version updates only. They stay in the dependency
# graph and a published advisory against any of them still raises an alert, exactly as with
# Selenium.WebDriver in Legacy/ below.
- dependency-name: "Microsoft.Bcl.AsyncInterfaces"
- dependency-name: "System.Text.Encodings.Web"
- dependency-name: "System.Threading.Tasks.Extensions"
- dependency-name: "System.Runtime.CompilerServices.Unsafe"
- dependency-name: "System.Buffers"
- dependency-name: "System.Memory"
- dependency-name: "System.ValueTuple"

- package-ecosystem: "nuget"
directory: "/Build/Dependencies/Legacy"
schedule:
Expand Down
142 changes: 117 additions & 25 deletions .github/workflows/dependency-lock-sync.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
name: Dependency Lock Sync

# Refreshes OmadaWeb.PS/DependencyLock.psd1 after a version changed in Build/Dependencies.
# Refreshes OmadaWeb.PS/DependencyLock.psd1 and Build/Dependencies.psd1 after a version changed in
# Build/Dependencies.
#
# The module verifies every binary it downloads against the SHA-256 pinned in the lock file, so a
# version bump is only half an update: the hash has to be recomputed from the newly published
# package. PR Validation fails while the two disagree, so this is the step that makes such a pull
# request mergeable.
# version bump is only part of an update: the hash has to be recomputed from the newly published
# package, and the SBOM has to report the version that is actually loaded. PR Validation fails while
# any of the three disagree, so this is the step that makes such a pull request mergeable.
#
# Run it from the Actions tab against the branch that carries the bump - typically a Dependabot
# branch - or do the same thing locally with:
# It runs weekly, a few hours after Dependabot's Monday run, over every open Dependabot pull request
# against Build/Dependencies - so a bump arrives already synced rather than waiting for someone to
# notice a red pull request. It can also be started by hand from the Actions tab for a single branch,
# which is what to use after a rebase. The same thing locally is:
#
# ./Build/Update-DependencyLock.ps1 -Refresh
#
# Deliberately NOT triggered automatically on Dependabot's pull requests. Workflows triggered by
# Dependabot run with a read-only GITHUB_TOKEN that the permissions key cannot elevate, so an
# automatic push would fail with 403 on exactly the pull requests it is meant to serve. Making it
# work anyway would mean pull_request_target with a checkout of the pull request head, which hands a
# write-scoped token to code from the branch under review - not a trade this module should make to
# save one click. The -Check gate in PR Validation still blocks an unsynced bump from merging.
# Two things worth knowing before reading the jobs below.
#
# Trust. The sweep pushes to a branch it did not write, so it must not *execute* anything from that
# branch. It checks out two trees - the default branch and the pull request branch - and runs the
# script from the default branch against the other one as data, via -RepositoryRoot. Nothing from the
# pull request branch runs with the write-scoped token. This is also why the workflow is triggered by
# schedule and workflow_dispatch rather than by the pull request itself: both run in the context of
# the default branch, so the token is a normal one and no pull_request_target is involved.
#
# Consequences on the pull request. Pushing to a Dependabot branch stops Dependabot from rebasing it
# automatically; comment `@dependabot recreate` to get a fresh one. And because PR Validation is
# triggered by an `/validate` comment rather than by a push, syncing the lock does not revalidate the
# pull request - a maintainer still comments `/validate`, and the -Check gate still blocks an unsynced
# bump from merging. The human decision to take a version is unchanged; only the mechanical part is
# automated.

on:
workflow_dispatch:
Expand All @@ -26,41 +38,121 @@ on:
description: 'Branch carrying the version bump (e.g. dependabot/nuget/Build/Dependencies/Selenium.WebDriver-4.48.0)'
required: true
type: string
schedule:
# Monday 06:00 UTC. Dependabot is scheduled for Monday in .github/dependabot.yml and opens its
# pull requests in the small hours, so by now there is something to sync.
- cron: '0 6 * * 1'

permissions:
contents: write
contents: read

concurrency:
group: dependency-lock-sync-${{ inputs.branch }}
cancel-in-progress: true
# A manual run for one branch is independent of the weekly sweep, so it gets its own group. Two
# runs are never cancelled in favour of each other: the loser would leave a branch half-synced.
group: dependency-lock-sync-${{ github.event.inputs.branch || 'sweep' }}
cancel-in-progress: false

jobs:
discover:
name: Find branches to sync
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
branches: ${{ steps.list.outputs.branches }}
count: ${{ steps.list.outputs.count }}

steps:
- name: List the branches carrying a bump
id: list
uses: actions/github-script@v9
with:
script: |
// workflow_dispatch names one branch explicitly and skips discovery entirely.
if (context.eventName === 'workflow_dispatch') {
const branch = context.payload.inputs.branch;
core.setOutput('branches', JSON.stringify([branch]));
core.setOutput('count', '1');
return;
}

const pulls = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});

// Only Dependabot's own branches, and only those touching the manifests this lock
// tracks. Anything else either has no pins to refresh or is not ours to push to.
const branches = pulls
.filter(pull => pull.user.login === 'dependabot[bot]')
.filter(pull => pull.head.repo?.full_name === `${context.repo.owner}/${context.repo.repo}`)
.filter(pull => pull.head.ref.startsWith('dependabot/nuget/Build/Dependencies'))
.map(pull => pull.head.ref);

core.info(`Branches to sync: ${branches.length ? branches.join(', ') : '(none)'}`);
core.setOutput('branches', JSON.stringify(branches));
core.setOutput('count', String(branches.length));

sync:
name: Refresh pinned hashes
needs: discover
if: needs.discover.outputs.count != '0'
runs-on: windows-latest
permissions:
contents: write
strategy:
# One failing branch must not stop the others from being synced.
fail-fast: false
matrix:
branch: ${{ fromJson(needs.discover.outputs.branches) }}

steps:
- name: Checkout the branch
- name: Checkout the default branch
# The trusted tree. Only the script from here is ever executed.
uses: actions/checkout@v7
with:
path: trusted
persist-credentials: false

- name: Checkout the branch carrying the bump
# The untrusted tree. Read as data, written to, pushed - never executed.
uses: actions/checkout@v7
with:
ref: ${{ inputs.branch }}
ref: ${{ matrix.branch }}
path: bump

- name: Refresh the dependency lock
- name: Refresh the dependency lock and the SBOM
shell: pwsh
run: ./Build/Update-DependencyLock.ps1 -Refresh
# The Dependabot configuration is read from the trusted tree rather than from the branch,
# because that is the copy Dependabot itself obeys: it only ever reads dependabot.yml from the
# default branch. Taking it from the branch would fail the closure ignore-policy check on any
# bump opened before those rules landed - refusing to sync a branch over a rule that does not
# apply to it. PR Validation still checks the branch's own copy, where it belongs.
run: >-
./trusted/Build/Update-DependencyLock.ps1 -Refresh
-RepositoryRoot (Resolve-Path ./bump).Path
-DependabotConfigPath (Resolve-Path ./trusted/.github/dependabot.yml).Path

- name: Commit the refreshed lock
- name: Commit the refreshed pins
shell: pwsh
working-directory: bump
# The branch name arrives through the environment rather than being interpolated into the
# script, so a branch whose name contains shell metacharacters cannot become code here.
env:
BUMP_BRANCH: ${{ matrix.branch }}
run: |
git config user.email "devops@fortigi.nl"
git config user.name "GitHub Actions"

$changed = git status --porcelain -- OmadaWeb.PS/DependencyLock.psd1
$changed = git status --porcelain -- OmadaWeb.PS/DependencyLock.psd1 Build/Dependencies.psd1
if ([string]::IsNullOrWhiteSpace($changed)) {
Write-Host "Lock file already matches the manifest; nothing to commit."
Write-Host "Pins already match the manifest; nothing to commit."
exit 0
}

git add OmadaWeb.PS/DependencyLock.psd1
git commit -m "deps: refresh pinned SHA-256 hashes for the bumped package"
git push origin HEAD:${{ inputs.branch }}
git add OmadaWeb.PS/DependencyLock.psd1 Build/Dependencies.psd1
git commit -m "deps: refresh pinned SHA-256 hashes and SBOM versions for the bumped package"
git push origin "HEAD:$env:BUMP_BRANCH"
14 changes: 7 additions & 7 deletions Build/Dependencies.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
Publisher = "Microsoft"
Purl = "pkg:nuget/Microsoft.Bcl.AsyncInterfaces"
Version = "8.0.0"
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json 8.0.5 closure, and verified by SHA-256 before loading."
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json closure, and verified by SHA-256 before loading. It moves only when System.Text.Json moves, so .github/dependabot.yml ignores version updates for it."
LockId = "Microsoft.Bcl.AsyncInterfaces"
Source = "https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces"
Website = "https://dot.net/"
Expand All @@ -169,7 +169,7 @@
Publisher = "Microsoft"
Purl = "pkg:nuget/System.Text.Encodings.Web"
Version = "8.0.0"
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json 8.0.5 closure, and verified by SHA-256 before loading."
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json closure, and verified by SHA-256 before loading. It moves only when System.Text.Json moves, so .github/dependabot.yml ignores version updates for it."
LockId = "System.Text.Encodings.Web"
Source = "https://www.nuget.org/packages/System.Text.Encodings.Web"
Website = "https://dot.net/"
Expand All @@ -187,7 +187,7 @@
Publisher = "Microsoft"
Purl = "pkg:nuget/System.Threading.Tasks.Extensions"
Version = "4.5.4"
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json 8.0.5 closure, and verified by SHA-256 before loading."
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json closure, and verified by SHA-256 before loading. It moves only when System.Text.Json moves, so .github/dependabot.yml ignores version updates for it."
LockId = "System.Threading.Tasks.Extensions"
Source = "https://www.nuget.org/packages/System.Threading.Tasks.Extensions"
Website = "https://dot.net/"
Expand All @@ -205,7 +205,7 @@
Publisher = "Microsoft"
Purl = "pkg:nuget/System.Runtime.CompilerServices.Unsafe"
Version = "4.5.3"
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json 8.0.5 closure, and verified by SHA-256 before loading."
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json closure, and verified by SHA-256 before loading. It moves only when System.Text.Json moves, so .github/dependabot.yml ignores version updates for it."
LockId = "System.Runtime.CompilerServices.Unsafe"
Source = "https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe"
Website = "https://dot.net/"
Expand All @@ -223,7 +223,7 @@
Publisher = "Microsoft"
Purl = "pkg:nuget/System.Buffers"
Version = "4.5.1"
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json 8.0.5 closure, and verified by SHA-256 before loading."
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json closure, and verified by SHA-256 before loading. It moves only when System.Text.Json moves, so .github/dependabot.yml ignores version updates for it."
LockId = "System.Buffers"
Source = "https://www.nuget.org/packages/System.Buffers"
Website = "https://dot.net/"
Expand All @@ -241,7 +241,7 @@
Publisher = "Microsoft"
Purl = "pkg:nuget/System.Memory"
Version = "4.5.5"
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json 8.0.5 closure, and verified by SHA-256 before loading."
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json closure, and verified by SHA-256 before loading. It moves only when System.Text.Json moves, so .github/dependabot.yml ignores version updates for it."
LockId = "System.Memory"
Source = "https://www.nuget.org/packages/System.Memory"
Website = "https://dot.net/"
Expand All @@ -259,7 +259,7 @@
Publisher = "Microsoft"
Purl = "pkg:nuget/System.ValueTuple"
Version = "4.5.0"
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json 8.0.5 closure, and verified by SHA-256 before loading."
VersionStrategy = "Pinned in OmadaWeb.PS/DependencyLock.psd1 as part of the System.Text.Json closure, and verified by SHA-256 before loading. It moves only when System.Text.Json moves, so .github/dependabot.yml ignores version updates for it."
LockId = "System.ValueTuple"
Source = "https://www.nuget.org/packages/System.ValueTuple"
Website = "https://dot.net/"
Expand Down
Loading