diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2d3e64c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +# the build stage needs the source and vendor/ only; everything else just slows the context upload +.git/ +.github/ +.idea/ +.claude/ +.tools/ +dist/ +report/ +data/ +prawn +prawn-env.sh +.prawn +*.db +*.db-shm +*.db-wal diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f4826b1..b32307e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -51,6 +51,62 @@ jobs: base64-subjects: ${{ needs.goreleaser.outputs.hashes }} upload-assets: true + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write # push the image to ghcr.io + id-token: write # cosign keyless signing via OIDC + attestations: write # actions/attest-build-provenance stores the attestation + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0 + - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # tag v1.2.3 publishes :1.2.3, :1.2 and :latest, plus :v1.2.3 so the image tag can be written + # the same way as the git tag + - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + id: meta + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=${{ github.ref_name }} + type=raw,value=latest + - name: read the go version to build with + id: go + run: echo "version=$(cat .go-version)" >> "$GITHUB_OUTPUT" + # the dockerfile cross-compiles from the build platform, so arm64 needs no qemu emulation + - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + id: push + with: + context: . + file: ./dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + GO_VERSION=${{ steps.go.outputs.version }} + VERSION=${{ github.ref_name }} + GIT_COMMIT=${{ github.sha }} + - name: sign the image digest + env: + DIGEST: ${{ steps.push.outputs.digest }} + run: cosign sign --yes "ghcr.io/${GITHUB_REPOSITORY}@${DIGEST}" + - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + homebrew: needs: goreleaser runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index f15cbab..4bdf2de 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.db-shm *.db-wal /report/ +/data/ .prawn .DS_Store .idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ef6bb..9720e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ - `explore` data tab: the columns are configurable — drag a header to reorder, × on a header removes it, **columns** adds any field collected on a PR (the review roll, CI, dates, decision, milestone, waiting, resolve time, closes, checks... ~45 of them); the order lives in the url (`dc=`) so saved views, download, upload, paste, and the AI prompt cover the table as they do the trends - `explore` collects what [ghp-sync](https://github.com/katbyte/ghp-sync) stamps on a project: **reviewed by**, **approved by**, **changes requested by** with request and review-comment counts (`kat(×2 ✎10)`), review comment totals, a **status** (merged, closed, or an open PR's state today), and **ci** (the head commit's checks: passing, failing, running); every `fetch` refreshes mergeability and CI across the open set in its reconcile walk, since neither moves a PR's updated time; query keys `reviewedby approvedby changesby status ci reviewcomments` - `explore` trends: **quality by group** — member reviews, review comments, and changes-requested rounds per PR, and the share merged clean, by the author's group and the month the PR opened (resolved PRs only); **review load by group** — the same member work by the month it was left; presets for each, and a **partners: member work** preset — member reviews, review comments, and comments on the partner group's PRs by month, as lines and as stacked bars; `member reviews` and `member ✎` data columns and `memberreviews memberreviewcomments membercomments` query keys +- `explore --serve `: after writing the page, serve it until ctrl-c and print the addresses other machines on the network can open it on; the page is written beside its path and swapped in whole, so a page being served is never read half-written; every request is logged with the viewer's login when a login-aware proxy in front sets it (`X-Forwarded-User` and friends), the client address, path, status, size and time +- `explore`: the tabs moved into the header as a dropdown where the page's name was, and two are renamed — **data** is **prs**, **areas** is **services** (old links and saved views still open); the view chips are a **preset** dropdown (one is ever active) with shorter names and a new **ci failing** entry, which puts the saved-view controls on the same line, and the PR detail's review line shows the head commit's ci; the saved-view controls show on every tab, since a view is the filter every tab shares plus the prs columns and the trend panels; a **draft** toggle beside the states, on for drafts only (`dr=yes`); the prs tab's **group by** (`gb=`) puts the table in sections — suggested category (docs only, tests and ci only, dependency bumps, small fixes…), kind of change, service, court, status today, ci, author group, effort, author — each keeping the sort inside it +- `fetch`: a later timeline page holding something the token can't read (a review request to a team, without org access) no longer fails the run; the node is left empty, as the first page already did +- a container (`dockerfile`, `docker-compose.yml`, `make docker`): serves the explore page and refreshes it on a cron schedule (`EXPLORE_CRON`), reading the same `.prawn` file the cli does; the release workflow publishes it to `ghcr.io/katbyte/prawn`, signed with cosign and attested - the standard katbyte tooling, as tctest and terrafmt: CI workflows (build, tests, golangci-lint with azproviderlint, depscheck, govulncheck, CodeQL, actionlint, yamllint, shellcheck, typos), dependabot, goreleaser releases with cosign signing and SLSA provenance, a homebrew tap formula, dev tools pinned in `.tools/go.mod` and built by `make tools`, `SECURITY.md`, `CODE_OF_CONDUCT.md`; the stricter linters cleaned up (`slices.Sort`, unused receivers, `fmt.Fprint`) - `fetch --since`: backfills every PR closed or merged since the date in month windows, resumable, and stores every PR's full timeline (`events`, `commits` tables) with `merged_by`, `milestone`, `base_ref`, `head_ref`; long timelines page to completion - `PRAWN_GROUP_=login,login`: named groups of authors for the explore page, from the environment or `.prawn`; `PRAWN_MAINTAINERS` names the group that counts as maintainers (default `members`) diff --git a/README.md b/README.md index 9bb5d3d..339afa5 100644 --- a/README.md +++ b/README.md @@ -77,19 +77,35 @@ export PRAWN_GROUP_MEMBERS=katbyte,jackofallops # author groups, any number of export PRAWN_GROUP_PARTNERS=magodo,wodansson prawn fetch prawn explore # -> report/explore.html +prawn explore --serve 8765 # ...and serve it, for other machines on the network ``` -One self-contained page over every PR open at any point in the period, with a filter bar over six tabs: +One self-contained page over every PR open at any point in the period, with a filter bar, and six tabs picked from the dropdown in the header: -- **data** — the matching PRs as a table; pick, reorder, and sort the columns, open a row for the PR's timeline +- **prs** — the matching PRs as a table; pick, reorder, and sort the columns, **group by** suggested category, kind, service, court, status, ci, author group, effort, or author to tackle alike PRs together; open a row for the PR's timeline - **trends** — metrics over time: backlog, flow, review status, times, review load, and quality by author group - **suggested** — easy wins by category (docs only, approved but unmerged, small and unanswered, …) or by service - **people** — authors and reviewers -- **areas** — services, kinds of change, labels +- **services** — services, kinds of change, labels - **checks** — the close candidates, restricted to the filter Every control lives in the url, so a view is a link, and views can be saved, downloaded, and pasted. +## Docker + +A container that keeps the explore page fresh and serves it: `prawn explore --serve` runs on start, and +a cron job inside refreshes the page on a schedule (`EXPLORE_CRON`, set in `docker-compose.yml`). + +```bash +# .prawn holds GITHUB_TOKEN, PRAWN_REPO, PRAWN_SINCE, PRAWN_GROUP_... — the same file the cli reads +make docker # or `docker compose pull` once a release has published the image +docker compose up -d # -> http://localhost:8765/ ; the db and the page live in ./data +``` + +The first start of an empty `./data` walks the whole repo before the page appears. The page is open to +anyone who can reach the port, so put a login in front of it (oauth2-proxy, Cloudflare Access) before +exposing it beyond the local network. + ## Configuration All options can be passed as command-line flags, environment variables, or via a `.prawn` file (env format) diff --git a/assets/reports/explore.html.tmpl b/assets/reports/explore.html.tmpl index d2cfd11..4a0f9a0 100644 --- a/assets/reports/explore.html.tmpl +++ b/assets/reports/explore.html.tmpl @@ -28,6 +28,8 @@ header { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 18px; p header h1 { font-size: 18px; margin: 0; font-weight: 650; } header h1 span { color: var(--muted); font-weight: 400; } header h1 b { font-weight: 650; } +/* the tab, as the page's name: a dropdown dressed as the title */ +header h1 select { font: inherit; font-weight: 650; background: var(--surface); color: var(--ink); border: 1px solid var(--axis); border-radius: 6px; padding: 2px 6px; cursor: pointer; } header .sub { color: var(--ink-2); font-size: 13px; } header .right { margin-left: auto; display: flex; gap: 8px; align-items: center; } button.plain, label.plain { border: 1px solid var(--axis); background: var(--surface); color: var(--ink-2); border-radius: 6px; padding: 3px 9px; font: inherit; font-size: 12px; cursor: pointer; } @@ -173,6 +175,8 @@ aside label.opt .lr button.on { background: var(--ink-2); color: var(--surface); .seg.multi button { display: inline-flex; align-items: center; gap: 5px; } .seg.multi button i { width: 8px; height: 8px; border-radius: 2px; display: inline-block; border: 1px dashed currentColor; } .seg.multi button.on i { background: var(--mc); border-color: var(--mc); } +.seg.multi.apart { margin-left: 8px; } /* the draft toggle: beside the states, not one of them */ +#filters label .row { display: flex; align-items: center; } select { border: 1px solid var(--axis); background: var(--surface); color: var(--ink); border-radius: 6px; padding: 3px 6px; font: inherit; font-size: 12px; } .pillbtn { font-size: 12px; padding: 2px 9px; border-radius: 6px; border: 1px solid var(--axis); background: var(--surface); color: var(--ink-2); cursor: pointer; user-select: none; } .pillbtn.on { color: var(--surface); background: var(--ink-2); border-color: var(--ink-2); } @@ -248,6 +252,10 @@ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; white-sp #colpick .pop { width: 360px; } #colpick .opt .desc { color: var(--muted); font-size: 11px; margin-left: auto; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px; flex: none; } #colpick .opt.reset { border-bottom: 1px solid var(--grid); border-radius: 0; margin-bottom: 4px; padding-bottom: 6px; } +/* group by: a section header per group, the count and the grouping's note beside the name */ +tr.group td { font-weight: 650; color: var(--ink); background: var(--page); padding: 10px 10px 5px; border-top: none; } +tr.group td .n { color: var(--muted); font-weight: 400; margin-left: 7px; font-variant-numeric: tabular-nums; } +tr.group td .desc { color: var(--muted); font-weight: 400; font-size: 12px; margin-left: 10px; } tr.pr { cursor: pointer; } tr.pr:hover td { background: var(--wash); } tr.pr td.title { max-width: 560px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -288,11 +296,11 @@ tr.detail td { background: var(--page); padding: 10px 14px 12px; }
-

🦐 prawn · explore · {{ .Repo }}

+

🦐 prawn · · {{ .Repo }}

{{ .Count }} PRs open at some point since {{ .Since }}