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
113 changes: 95 additions & 18 deletions .github/workflows/dependabot-merge.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,108 @@
name: Dependabot auto-merge
on: pull_request

# Merges a Dependabot PR only once CI has actually reported green.
#
# This used to run `on: pull_request` and call `gh pr merge --auto`. That flag reads like
# "merge when green", but it means "merge when branch protection is satisfied" — it delegates
# the waiting entirely to required status checks. `main` is unprotected and declares none, so
# the condition was vacuously true the instant the PR opened and the merge landed immediately.
# PR #233 was opened at 04:18:08 and merged at 04:18:20; its test jobs did not start until
# 04:18:34 and went red at 04:21. That is how a failing dependency bump reached main.
#
# Rather than turn on branch protection, the list of checks that must be green lives here.
# Firing on workflow_run instead of pull_request is what makes that possible: the merge is
# decided after CI reports, so there is no check to wait on that is waiting on us, and no
# runner sits idle for the duration of the build.

on:
workflow_run:
workflows:
- ".NET Workflow"
- "Verify Generated Files"
types: [completed]

permissions:
contents: write
pull-requests: write
packages: read

jobs:
dependabot:
name: Process Dependabot PR
merge:
name: Merge green Dependabot PR
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
timeout-minutes: 5

# Only what the event itself is, never who it claims to be. A successful CI run on a pull
# request is the cheap pre-filter; whose pull request it is gets decided in the step below,
# from the API. A failed run is not merely "not yet" — it must never reach the merge step.
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'

steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"

- name: Enable auto-merge for Dependabot PRs
if: steps.metadata.outputs.update-type == 'version-update:semver-major' || steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'
- name: Merge if Dependabot opened it and every check on the commit is green
env:
PR_URL: ${{github.event.pull_request.html_url}}
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHA: ${{ github.event.workflow_run.head_sha }}
SELF: Merge green Dependabot PR
run: |
set -e
echo "Processing PR: $PR_URL"
gh pr merge --auto --merge "$PR_URL"
set -euo pipefail

# The workflow_run payload says who triggered the run, and that is not an
# authenticated statement about who opened the pull request: a re-run re-attributes
# it to whoever pressed the button, and nothing in the payload is signed. Trusting
# `actor.login` or the `dependabot/` branch prefix to decide what may be merged is
# githubactions:S8232. So the payload is used only to locate the pull request, and
# GitHub is asked who actually opened it.
#
# Matching on the head SHA rather than the branch name also buys the property this
# workflow exists for: the only pull request that can match is one whose head is the
# exact commit CI just reported on. A commit pushed after CI finished moves the head,
# nothing matches, and nothing is merged — instead of merging code CI never saw.
case "$SHA" in
"" | *[!0-9a-f]*) echo "Head SHA is not a hex digest; refusing to continue." >&2; exit 1 ;;
esac

FOUND=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls?state=open&per_page=100" \
--jq "[.[] | select(.head.sha == \"$SHA\")][0]
| select(. != null)
| [.number, .user.login] | @tsv")

if [ -z "$FOUND" ]; then
echo "No open pull request whose head is $SHA; nothing to merge."
exit 0
fi

PR=$(printf '%s' "$FOUND" | cut -f1)
AUTHOR=$(printf '%s' "$FOUND" | cut -f2)

if [ "$AUTHOR" != "dependabot[bot]" ]; then
echo "PR #$PR was opened by $AUTHOR, not Dependabot. Leaving it alone."
exit 0
fi

# The workflow that triggered us is green, but it is not the only one. Ask the
# commit what every check says, and treat anything still running as a reason to
# wait rather than a reason to merge. A check that is absent entirely is not
# counted: Verify Generated Files skips a markdown-only bump by design.
#
# skipped and neutral count as green. dotnet.yml skips Analyze & Release and
# Security Scanning on a pull request, and CodeQL reports neutral there.
gh api --paginate "repos/$GITHUB_REPOSITORY/commits/$SHA/check-runs" \
--jq ".check_runs[]
| select(.name != \"$SELF\")
| select(.status != \"completed\"
or (.conclusion != \"success\"
and .conclusion != \"skipped\"
and .conclusion != \"neutral\"))
| \" \(.name): \(.status)/\(.conclusion // \"pending\")\"" > "$RUNNER_TEMP/not-green.txt"

if [ -s "$RUNNER_TEMP/not-green.txt" ]; then
echo "Not merging PR #$PR. These checks are not green:"
cat "$RUNNER_TEMP/not-green.txt"
# Not a failure of this workflow: whichever run finishes last fires it again,
# and the merge happens on that pass.
exit 0
fi

echo "PR #$PR is Dependabot's and every check on $SHA is green. Merging."
gh pr merge --merge "$PR"
17 changes: 11 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ forms as well, and matching it is a change to every form at once rather than par

### Can the quantities be generated for languages other than C# and C++?

`SevenTargetProjectionTests` is the probe that answers it, and the answer so far is **five of
`SevenTargetProjectionTests` is the probe that answers it, and the answer so far is **six of
seven**. It builds one quantity — the magnitude form of `Length`, read from the real
`dimensions.json` — as a language-agnostic `ktsu.Coder` AST and writes it in all seven of that
library's targets. The neutral shape is the whole of what a generated magnitude is: a record struct
Expand All @@ -210,16 +210,21 @@ C# comes out as exactly what `QuantitiesGenerator` writes today, which is the re
the AST is expressive enough for the quantities, so what the other six do is a question about those
languages rather than about the model.

**Two targets write source their own toolchain refuses**, both recorded upstream and neither fixable
here:
**One target writes source its own toolchain refuses**, recorded upstream and not fixable here:

| Target | What comes out | Why |
|---|---|---|
| Go | `type Length struct` with no parameters, then `func LengthFromMeter(value T) Length[T]` | ktsu-dev/Coder#63 — a generic type is deliberately written down rather than emitted, and the constructor was not given the same treatment. `go vet` says `undefined: T`. |
| Python | `class Length(IVector0[Length[T], T])` | ktsu-dev/Coder#64 — Python evaluates a base list eagerly, so the self-type idiom every quantity is declared with raises `NameError` on import. |

The tests **pin** both rather than skipping them, so the day either is fixed upstream the test fails
and is updated to assert the fix.
The test **pins** it rather than skipping it, so the day it is fixed upstream the test fails and is
updated to assert the fix.

**Python was the second, and the pin is what caught the fix.** It wrote
`class Length(IVector0[Length[T], T])`, which raises `NameError` on import because Python evaluates
a base list eagerly. ktsu-dev/Coder#64 fixed that to the string forward reference
`IVector0["Length[T]", T]`, and the first build after the bump from ktsu.Coder 3.14.0 to 3.14.3
failed here — which is the whole point of pinning rather than skipping, and is how the fix was
noticed at all. The test asserts the fix now, so a regression upstream fails the same way round.

The probe lives in `Semantics.Cpp.Test` because that is where the reader of `dimensions.json` is,
and that is itself the finding about this repository: `QuantityMetadata` and `MetadataProjection`
Expand Down
28 changes: 17 additions & 11 deletions Semantics.Cpp.Test/SevenTargetProjectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ namespace ktsu.Semantics.Cpp.Test;
/// before a neutral projection could be a project of its own.
/// </para>
/// <para>
/// <b>Two targets are known to produce source their own toolchain refuses</b>, and the tests below
/// pin that rather than skipping it, so the day either is fixed upstream the test fails and says
/// so. Both are recorded against ktsu.Coder — ktsu-dev/Coder#63 and ktsu-dev/Coder#64 — and
/// neither is anything this repository can fix.
/// <b>One target is still known to produce source its own toolchain refuses</b>, and the test
/// below pins that rather than skipping it, so the day it is fixed upstream the test fails and
/// says so. It is recorded against ktsu.Coder as ktsu-dev/Coder#63 and is not anything this
/// repository can fix. Python was the second such target until ktsu-dev/Coder#64 was fixed; its
/// pin failed exactly as intended, and it now asserts the fix.
/// </para>
/// </remarks>
[TestClass]
Expand Down Expand Up @@ -142,20 +143,25 @@ public void GoSpellsAGenericTypeItDidNotDeclare()
}

/// <summary>
/// Python writes a base that names the class being declared, which Python evaluates before the
/// class exists.
/// Python quotes the self-type in its own base list, which is what makes the class importable.
/// </summary>
/// <remarks>
/// `IVector0&lt;Length&lt;T&gt;, T&gt;` is the self-type idiom every quantity here is declared
/// with, and it is what C# needs to give an interface a method returning the implementing type.
/// Python evaluates a base list eagerly, so `class Length(IVector0[Length[T], T])` raises
/// `NameError: name 'Length' is not defined` on import. A string in the base list is the fix
/// Python has for this, and it is not something the AST currently says. ktsu-dev/Coder#64.
/// Python evaluates a base list eagerly, so naming the class inside its own bases —
/// `IVector0[Length[T], T]` — raises `NameError: name 'Length' is not defined` on import. A
/// string is the forward reference Python has for exactly this, and ktsu.Coder now writes one.
/// <para>
/// This was pinned as a defect until ktsu-dev/Coder#64 was fixed, which the bump from
/// ktsu.Coder 3.14.0 to 3.14.3 brought in. The pin failed, as it was written to, and asserts
/// the fix instead. Keeping it pins the fix the same way round, so a regression upstream fails
/// here rather than shipping a module that cannot be imported.
/// </para>
/// </remarks>
[TestMethod]
public void PythonNamesTheClassInsideItsOwnBases()
public void PythonQuotesTheClassInsideItsOwnBases()
{
Assert.Contains("class Length(IVector0[Length[T], T])", Written["python"]);
Assert.Contains("class Length(IVector0[\"Length[T]\", T])", Written["python"]);
}

/// <summary>
Expand Down
Loading