Attribution names the pull requests a stack claims and only counts the rest
Decision record 0026
Amended by 0037: the summary has a budget after all. When it is over, its list of pull requests is cut before any change line.
Amended by 0054: the title of a pull request reaches the issue on the row of an update waiting to merge, escaped and cut at 80 characters, and not with redact on.
A pending row says which merges made it pending. The brief put from #123 by @robbe on the checkbox line. Records 0003, 0010 and 0016 already fixed the frame: the range runs from the commit on the stack’s last successful deployment record, “touching a stack” is the claim rule, and a list that is too long after an outside deploy is accepted. This record decides the rest. Attribution explains a pending row. It never decides that a row is pending, it is not part of the diff hash (0008), and nothing about a deploy is ever decided from it.
The unit is the pull request, not the commit. One merge can land as one commit (squash), several (rebase) or a merge commit on top of its branch commits, and a reader asks “which merge did this”, not “which commit”. Every commit in the range is mapped to its merged pull request and each pull request is named once. A commit with no pull request, a direct push, is named by itself so it is never left out. Listing commits was rejected because a rebase merge of ten commits would fill the line with ten entries for one review.
A row names only the pull requests the stack claims. A change that touches only files no stack claims (a shared package, the lockfile) forces a full scan and can make every stack pending at once (0010). Naming it on every row was rejected: on a repo where a bot bumps the lockfile all day, every row would carry the same long list and the merges to a stack’s own files would drown in it. Those changes are counted on the row, not listed, and every attribution line ends with a link to GitHub’s compare view from the last deployed commit to the scanned commit. The link costs no API call and always shows the whole truth, also when the list ran long after an outside deploy (0016).
- [ ] **apps/grafana:prod** · `+2 ~1 -0` · [preview](run-url) <!-- sluiceway:row stack="apps/grafana:prod" state="pending" hash="3fa9c1e2aabbccdd" --> from #140 by alice, #138 by renovate[bot], 3fa9c1e by bob, and 2 more, and 4 changes outside this stack · [compare](compare-url)Commits are mapped to pull requests with one GraphQL query that walks the newest commits of the default branch and returns, for each, its parents, its author, its merged pull requests, and each pull request’s author and changed file paths. Tested against this repo, 100 commits cost 3 points of the workflow token’s 1,000 per hour. A merge commit and the branch commits under it all return the same pull request, so naming each once needs no extra rule. Parsing merge commit messages was rejected: a squash merge with an edited title has no number to find. The REST endpoint that lists the pull requests of one commit was rejected because it costs one call per commit.
Consequences
- The attribution line is its own line inside the row block, directly under the row’s first line. No issue or pull request reference ever sits on the checkbox line (0009).
- The range starts at the commit on the stack’s last successful deployment record and ends at the
scan-shaon the root marker, not at the newest commit. The row’s diff was previewed atscan-sha, and the line explains exactly that diff. - A stack with no successful deployment record inside the bounded reads of 0003 gets no guess. The line reads
not deployed from this dashboard yetand lists nothing. This covers the day a repo adopts Sluiceway, a stack that was moved or renamed (0006), and a record too old to be found. No extra pages are read to look for one. “The last N commits” and “since the directory was created” were rejected, because both name merges that may have gone live long ago by other means. - The lookback is the newest 100 commits on the default branch, walked once per job and shared by every stack. It is fixed in v1. When a stack’s starting commit is not inside the lookback, the row lists what was found and adds
and earlier changes. The compare link still covers the whole range. - A commit is in range when it is inside the lookback and cannot be reached from the starting commit by following parents inside the lookback. The walk lists children before parents, so every path from the starting commit to an older commit in the window lies inside the window, and no compare call per stack is needed. This is correct with merge commits. After a force push the starting commit is simply not in the window and the row reads
and earlier changes. - A commit belongs to its merged pull request whose base is the default branch. A commit with none is a direct push. Its files cost one REST call, the only per-commit call there is. The worst case, a repo that only pushes directly, is about 100 requests per job.
- The claim rule (0010) is applied to the pull request’s changed files, or to the commit’s files for a direct push. A stack claims a pull request when it claims at least one of its files. A pull request with more than 100 changed files is not paged through. It counts as a change outside the stack, which errs on the side of never hiding it.
- A change counts as outside a stack when the stack does not claim it and it holds at least one file that no stack claims. That is the kind of change that forces a full scan and may reach any stack. It adds one to
changes outside this stackon every row whose range holds it. A change that only other stacks claim cannot reach this stack under the claim rule and is not counted on its row. Files matched byscan.unrelated(0010) claim nothing and count for nothing. - Because attribution and narrowing use one rule, record 0011 stays true: a row a narrowed scan carries through byte for byte never lacks a pull request that a full scan would have named.
- The order of the line is fixed: up to five claimed pull requests or direct pushes, newest first, then
and N morewhen some were cut, thenand N changes outside this stack, thenand earlier changes, then the compare link. A row pending only through shared changes readsfrom 4 changes outside this stack · compare. Five is fixed in v1. - A pull request is written as a bare
#123, which gives GitHub’s hover card with title and state, and puts a one-time backlink to the dashboard on the pull request. A direct push is its short SHA as a link. - The author is the pull request’s author, not who merged it, and for a direct push the commit’s author. The login is plain text with no
@. An@mention in an issue body notifies that person and subscribes them to the issue, also when a bot writes it, and the dashboard is a place people come to, not something that pings them. This departs from the brief. A commit author with no GitHub account is left unnamed. - No pull request title or commit message appears on the dashboard. Titles are free text of any length, would eat the body budget and would need escaping. The summary page has no budget and no hover cards, so it lists every pull request in range without a cut, as a full link with its escaped title, and every direct push with the first line of its message.
bydirectly after a pull request or commit always means the author. The ticker (0025) is always writtenticked by, never a bareby. The ticker never appears on the attribution line and an author never appears on the failure line. When one person is both, both are written, because they are two facts. Nothing new is stored: authors are recomputed and the ticker comes from the deployment record.- A deploying row keeps its attribution line, so a reader sees what is going out.
- The visible text of a row is never parsed (0009), so a writer cannot copy an old attribution line. Every job that renders a row (
scan,resolve,apply,settle) works attribution out again with the one shared function in the core. It needs the workflow token only, soresolveandsettlestill run without tool credentials (0014). The glue hands the core the walked commits as data, as it does with the compare file list (0010). - Attribution never blocks. If the lookup fails, the row is written without an attribution line and the job carries on. A scan or a deploy never fails because of it.
- When the body is over budget, the named pull requests on a row are cut before any line of its diff, and the compare link is cut last. Destroys keep their guaranteed place (0024).
- A redacted dashboard keeps the attribution line as it is (0023). It holds no resource names. No property value is involved anywhere (0021).
- Recently deployed does not say which pull requests a deploy shipped. It would need the list stored on the record at tick time, or a walk between the two newest successful records of each stack that may lie outside the lookback. The record already carries the deployed commit (0003), so this can be added without a breaking change.
- Not in v1, each with a line in
docs/later.md: telling authors that their merge is waiting, shipped pull requests under recently deployed, a configurable lookback or number of names, and naming changes outside a stack on its row.
Settled while building (slice 2.8)
- The walk starts at the scanned commit, not at the newest commit of the branch: it is the history of
scan-sha, 100 commits back. For a scan of the default branch that is the same list without the commits that came afterscan-sha, which the range never holds anyway. The default branch is still asked for by name, because a commit belongs to its merged pull request whose base is that branch. - The query costs 7 points of the 1,000 per hour, not 3. It asks for up to five pull requests per commit, so that a commit that came in through a pull request into a branch and then through that branch’s pull request still finds the one whose base is the default branch. Measured against this repo on 2026-09-21 with 100 commits.
- For a commit GitHub does not have, the query answers with no object and no error. The port reads that as a failed lookup.
- GraphQL names the author of a bot’s pull request without the
[bot]that the site and REST write (renovate, typeBot), and names the user of a bot’s commit with it. The port adds it, so a row reads#138 by renovate[bot]however the change landed. - GraphQL gives only the new path of a file a pull request renamed. A direct push is read over REST, which gives both, and both go to the claim rule as they do for the narrowed scan (0010). So a pull request that only moves files out of a stack is not named on that stack’s row. The compare link shows it.
- The REST read of one commit gives up to 300 files when it is asked without a page size. A direct push with 300 files may be missing some, so it counts as a change outside every stack, the same way a pull request with more than 100 changed files does.
- A row with nothing to name and nothing to count reads
nothing this stack claims has changed since its last deploy · compare. That is a row whose last deploy ran on the scanned commit or on a newer one, or whose range holds only changes that other stacks claim. The preview still found a diff, which is what an outside change to the state or a provider looks like. - At budget level 1 the count says pull requests and direct pushes apart:
from 3 pull requests and 1 direct push, and 4 changes outside this stack · compare. - The compare link holds the first 12 characters of each commit id. Two whole ids are 56 characters more on every row of a body with a size budget, and seven characters can be ambiguous in a large repo, where GitHub then answers 404.
- A login is escaped like every other text from outside. A managed account has underscores in its login, and two of them on one line would set the text between them in italics.
- The starting commit is the commit of the stack’s newest success among the records the bounded reads of 0003 found. When the newest record of a stack is a failure that only the REST fall back found, an older success is not looked for, and the row reads
not deployed from this dashboard yet. - No request is made when no row has a commit to start from, and none when no stack is pending or deploying. The files of a direct push are read only when it is in the range of such a row.
- A failed lookup is not tried again in the same job. The dashboard is written in a loop, and a row that has the line on one try and not on the next helps nobody. The job log says once what GitHub answered. There is no annotation, because a row without the line is still true.
- The scan writes its summary before the late read, and the starting commit comes from the late read. So a scan that found pull requests writes the summary once more after the dashboard, with the list under each stack.
- A deploying row that a scan makes from the deployment record has the line too, worked out to the scan’s own commit like every other row of that scan.
- The workflows in the README ask for
pull-requests: read. Apermissionsblock sets every permission it does not name to none, and the walk reads pull requests. What a workflow token without it gets on a private repo was not observed. Either way the scan carries on. resolveworks the line out again for the deploying row it writes, and for a deploying row it repairs from an open record. The range ends at thescan-shaon the root marker of the live body, as the record says. That value is text a person can edit, so ascan-shathat is not a whole commit id starts no request and no link, and the row is written without the line. The walk is kept perscan-shafor the run, so a write that takes three tries walks once.- The record that
resolvehas just created is now the newest record of its stack, so the REST fall back of 0003 can no longer find an older success. A stack whose last success has left the page of its environment readsnot deployed from this dashboard yeton its deploying row, although its pending row named pull requests. This needs more than 100 records in one environment since that success.