Skip to content

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.showValues lists, 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: write and 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>, its html_url. GitHub ignores the details_url the 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.summary and output.text each take at most 65,535 characters. summary is also refused over 65,535 bytes. text over 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. neutral counts as success in the pull request rollup.
  • Two creates with one name make two runs, and filter=latest lists 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 preview link is the page’s html_url. This supersedes record 0044’s target for a pending row, and record 0048’s too: with scan.logDiff on the link still lands on the page, and the page says where the tool’s diff is. Every other link of 0044 stays: the summary link of a shortened or redacted row, a preview failure’s run, 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.logDiff on, 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 that checks: write gives the rows their pages. It is a line in the log, not an annotation, because a repo that moves to 0.2.0 through @v0 without 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. apply writes 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. apply would need checks: write too, 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 and docs/credentials.md add it to the workflow’s permissions: block, next to what 0017 lists. resolve, apply and settle do 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. resolve and settle run nothing but Sluiceway, and apply runs the same code the scan does.
  • With checks: write a token can create a check run of any name on any commit of the repo, success included, 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.md says 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 is neutral, 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’ summary links, and the full list for a page that was cut.

Research: