A pending row links to a preview page: a check run with the stack's diff
Decision record 0050
Amended by 0059: a drifted stack gets a preview page too, which lists its drift like a pending stack’s changes, and the page of a pending stack that also drifted lists the drift after the changes.
Amended by 0052: the page shows the old and new value at the paths that
dashboard.showValueslists, as the row does, and says so in its summary. That is the separate decision this record asked for.
The first real user asked why the preview is not there when they click the link (onboarding log, hurdle 18, part C). Record 0044 sends a pending row’s preview link to the summary of the attempt, where the stack is one section among all the others, and record 0048 sends it to the job log when scan.logDiff is on. Neither is a page of that stack. The owner decided on 2026-09-22 that a GitHub check run per pending stack becomes that page, for 0.2.0. The research on the branch research/preview-page (docs/research/preview-page.md) measured what a check run made with the workflow token can and cannot do. This record fixes how Sluiceway uses one.
What was measured (research, 2026-09-22)
- The workflow token can create a check run with
checks: writeand answers 403 “Resource not accessible by integration” without it. One POST creates a finished run with its output. - Its page is
https://github.com/<owner>/<repo>/runs/<id>, itshtml_url. GitHub ignores thedetails_urlthe token sends and sets it to that same address. The address belongs to the id, and a re-run of the workflow does not move it. A public repo shows it logged out. A private repo gives a logged-out person a 404. output.summaryandoutput.texteach take at most 65,535 characters.summaryis also refused over 65,535 bytes.textover 65,535 bytes is cut without an error, anywhere in a line. A PATCH replaces the whole output.- It joins the oldest GitHub Actions check suite on the commit, whichever workflow made it, and that run’s jobs list shows it as a job.
neutralcounts as success in the pull request rollup. - Two creates with one name make two runs, and
filter=latestlists only the newest. Updating by name keeps one. - The runner’s masks do not apply. A string registered with
::add-mask::came back in plain text from the check run’s output. - There is no DELETE. Deleting the workflow run whose suite holds a check run deletes the check run too.
Decision
- A scan writes one preview page per pending stack on the commit it scanned: a check run named
sluiceway / <stack id>,status: completed,conclusion: neutral. A stack in sync and a preview failure get none. A narrowed scan writes pages only for the pending stacks it previewed. A carried row keeps the link it had. - Find and update in place. The scan lists the commit’s check runs once, with
filter=latest, 100 to a page, and matches by name. A page that exists is updated with a PATCH of its output, and a missing one is created. So a scan of a commit that was scanned before adds no page. The cost is one list request per 100 check runs on the commit, plus one write per pending stack. A page that is gone since the list, because the run it sat in was deleted, is created again. - The row’s
previewlink is the page’shtml_url. This supersedes record 0044’s target for a pending row, and record 0048’s too: withscan.logDiffon the link still lands on the page, and the page says where the tool’s diff is. Every other link of 0044 stays: thesummarylink of a shortened or redacted row, a preview failure’srun, the summary’s index and anchors, and every link of a row written from a deployment record. - The page shows Sluiceway’s own diff and never a value. The title is the stack id and its counts. The summary holds the counts, the destroy warning, a link to the dashboard, a link to the summary of the scan, a link to the job log and, with
scan.logDiffon, a line that says the tool’s own diff is in the job log, in the stack’s group, and not on the page. The text lists every change, destroys first, with every property path whole, as the summary does. Nothing on the page comes from the tool’s text, because the runner’s masks do not reach it. Showing values there would need a decision of its own that amends this record, 0021 and 0048. - The text is cut by Sluiceway, on a whole line, when it would be over 65,535 UTF-8 bytes, which keeps it inside both of GitHub’s limits. The last line then says how many changes are not listed and points to the job log, which holds every change, and to the summary. Destroys are listed first, so they are cut last. The summary is never cut: a stack id is a path and a name from the repo’s files.
- The dashboard link is the issue list with the dashboard’s label,
/issues?q=is:issue is:open label:"<label>". The page is written before the dashboard, and a first scan does not know the dashboard’s number yet. The one open issue with that label is the dashboard. - Without
checks: write, or when GitHub refuses a page for any reason that every later write would meet too (a 403 or a 429), the scan stops writing pages for the rest of the job after the first refusal. Every pending row’s link then lands where records 0044 and 0048 send it, and the job log says, once, what GitHub answered and thatchecks: writegives the rows their pages. It is a line in the log, not an annotation, because a repo that moves to 0.2.0 through@v0without changing its workflow would otherwise get a warning on every run. A page that fails on its own, for another reason, costs only its own row the link, and the log names it. A page never stops a scan and never turns it red. - Only the scan writes pages.
applywrites a fresh pending row after a change that moved, and that row keeps the link of 0044 or 0048. The next scan gives it a page.applywould needchecks: writetoo, for a row that lives until the next push.
Where names now appear (amends 0021)
Record 0021 lists the places Sluiceway writes. The preview page is one more, with the same content as the summary: resource types and names, ops, tracking changes and property paths. It is not redacted, like the summary (0023), because the same people can read it: whoever can read the repo in the web interface. Through the API any token or app that can read checks on the repo reads it too, as JSON. The page’s name, sluiceway / <stack id>, also shows in the commit’s list of checks, in the jobs list of the workflow run whose suite the page joined and, when that suite came from a push, in the pull request’s checks. Stack ids are already on the dashboard, redacted or not.
Consequences
- The scan job needs
checks: write. The README, the example workflows anddocs/credentials.mdadd it to the workflow’spermissions:block, next to what 0017 lists.resolve,applyandsettledo not use it. It sits on the workflow’s block, not on the scan job alone, because a job’s block replaces the workflow’s and every user would have to repeat the whole block for one line.resolveandsettlerun nothing but Sluiceway, andapplyruns the same code the scan does. - With
checks: writea token can create a check run of any name on any commit of the repo,successincluded, from the same GitHub Actions source as every CI job, and can rewrite the output of any check run of GitHub Actions. It cannot change the conclusion of a job that Actions runs.docs/security.mdsays so: a repo that requires checks for merging should know that any job of this workflow can now write a passing check under any name. - The page shows up where Sluiceway does not choose: as a job of whichever workflow run has the oldest check suite on the commit, and in a pull request’s checks when that suite came from a
push. It isneutral, so it never fails a rollup. The README says so, because it will be reported as a bug. - A page lives as long as the repo’s Actions retention (from 2026-10-01; 400 days before), and ends early when someone deletes the run whose suite it joined. It cannot be deleted, only blanked. A page of a stack that is no longer pending stays as it was on its commit: a scan of the same commit does not blank it in v1 (
docs/later.md). It shows no value, so what stays is what the summary of that scan showed. - The budget of 0017: a scan with pending stacks adds one list request per 100 check runs on the commit and one write per pending stack, once per scan, not per try of the write loop. The worst case of 0017 with 100 pending stacks becomes 405 requests on the first try and 807 with three tries, still under 1,000. A scan with nothing pending adds none.
- GitHub’s limit of 80 content-generating requests a minute was not measured for check runs. A refusal for it stops the pages, and the rows fall back.
- The summary keeps every role it had (0037, 0044): the index, the anchors, the shortened and redacted rows’
summarylinks, and the full list for a page that was cut.
Research: