~bigbes/ci-cacher

969662b88d348767a794537fc8aadc2255abfa76 — Eugene Blikh 2 days ago e8e2b2f
Add goreleaser + CHANGELOG.md; cross-platform downloads on site

* .goreleaser.yml — builds linux/{amd64,arm64} and darwin/{amd64,arm64}
  as raw binaries (formats: binary) with CGO_ENABLED=0, trimpath, version
  ldflag injection. GitHub release disabled; we publish via pages.sr.ht.

* CHANGELOG.md — Keep-a-Changelog format. v0.1.0 entry summarizes the
  full surface (init/doctor, single-file/docker/dir caching, --hash-from,
  Garage compat, e2e suite, CI manifests).

* .builds/publish.yml — installs goreleaser binary release (pinned to
  v2.7.0), runs `goreleaser release --clean --skip=publish`, flattens
  the 4 dist/cacher_<os>_<arch>_v<n>/cacher paths into
  pages/cacher-<os>-<arch>, regenerates checksums.txt to match the
  published filenames, renders CHANGELOG.md to HTML via cmark with
  heading demotion (h1→drop, h2→h3, h3→h4), substitutes per-platform
  sha256s into docs/index.html, and ships everything to
  bigbes.pages.srht.bigb.es/ci-cacher/ plus the build artifacts list.

* docs/index.html — Download section is now a 4-row table with per-
  platform sha256s, plus a Changelog section that embeds the rendered
  CHANGELOG.md inline.
4 files changed, 234 insertions(+), 39 deletions(-)

M .builds/publish.yml
A .goreleaser.yml
A CHANGELOG.md
M docs/index.html
M .builds/publish.yml => .builds/publish.yml +70 -32
@@ 1,26 1,25 @@
# Publish a linux-amd64 binary on every tag push. Two destinations:
# Build cross-platform binaries via goreleaser, ship them three ways on
# every tag push:
#
#   1. Build artifact (cacher-linux-amd64 visible on the job page,
#      pruned by builds.sr.ht after 90 days).
#
#   2. pages.sr.ht under bigbes.pages.srht.bigb.es/ci-cacher/, so
#      downstream projects can `wget` from a stable URL. We publish
#      with `hut pages publish -s /ci-cacher` so this manifest only
#      touches files under that subpath — other projects sharing the
#      user-level pages domain stay intact. Latest tag overwrites the
#      previous publish under /ci-cacher; historical versions remain
#      available via the artifact link during the 90-day window.
#   1. Build artifacts (visible on the job page, 90-day TTL).
#   2. pages.sr.ht under bigbes.pages.srht.bigb.es/ci-cacher/ via
#      `hut pages publish -s /ci-cacher` (subpath-scoped, so other
#      projects sharing the user-level pages domain stay intact).
#   3. A landing page rendered from docs/index.html with CHANGELOG.md
#      embedded inline.
#
# Auto-submission is restricted to tag refs only.
image: ubuntu/noble
packages:
  - curl
  - ca-certificates
  - cmark            # CHANGELOG.md → HTML
oauth: pages.sr.ht/PAGES:RW
sources:
  - https://git.srht.bigb.es/~bigbes/ci-cacher
environment:
  GO_VERSION: "1.26.3"
  GORELEASER_VERSION: "v2.7.0"
  PATH: /home/build/.local/go/bin:/home/build/.local/bin:/home/build/go/bin:/usr/local/bin:/usr/bin:/bin
  GOPATH: /home/build/go
  PAGES_DOMAIN: bigbes.pages.srht.bigb.es


@@ 39,38 38,77 @@ tasks:
      rm "/tmp/$GO_TARBALL"
      go version
  - install_hut: |
      # hut isn't in ubuntu/noble's repos; build it from source with the
      # Go we just installed. ~5s on a warm GOPATH, ~30s cold.
      go install git.sr.ht/~xenrox/hut@latest
      hut --version
  - install_goreleaser: |
      # Pinned binary release, not `go install` — goreleaser's release
      # binaries are stripped + statically linked and ~10x smaller than
      # a from-source build.
      curl -sSL "https://github.com/goreleaser/goreleaser/releases/download/${GORELEASER_VERSION}/goreleaser_Linux_x86_64.tar.gz" \
        | tar -xz -C ~/.local/bin goreleaser
      goreleaser --version
  - build: |
      cd ci-cacher
      VERSION=$(git describe --tags --abbrev=0 2>/dev/null || cat VERSION)
      echo "Building $VERSION"
      GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
        go build -ldflags "-s -w -X go.bigb.es/cacher/internal/version.version=${VERSION}" \
        -o /home/build/cacher-linux-amd64 .
      /home/build/cacher-linux-amd64 version
      sha256sum /home/build/cacher-linux-amd64
      goreleaser release --clean --skip=publish
      ls dist/
  - package_pages: |
      # pages.sr.ht expects a tarball whose top-level contents map to the
      # publish path — so `tar -C dist .`, not `tar dist/`. With
      # `-s /ci-cacher` the binary lands at
      # https://$PAGES_DOMAIN/ci-cacher/cacher-linux-amd64. docs/index.html
      # is the project landing page; the three {{PLACEHOLDERS}} get
      # substituted at publish time with the values from this build.
      # Goreleaser writes binaries under dist/cacher_<os>_<arch>_v<n>/cacher.
      # Flatten + rename to the pages-facing /ci-cacher/cacher-<os>-<arch>
      # naming. Regenerate checksums.txt against the renamed files so the
      # published file matches the URLs people will wget.
      VERSION=$(cd ci-cacher && (git describe --tags --abbrev=0 2>/dev/null || cat VERSION))
      SUM=$(sha256sum /home/build/cacher-linux-amd64 | awk '{print $1}')
      BUILT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
      mkdir -p /home/build/dist
      cp /home/build/cacher-linux-amd64 /home/build/dist/cacher-linux-amd64
      mkdir -p /home/build/pages
      cp ci-cacher/dist/cacher_linux_amd64_v1/cacher    /home/build/pages/cacher-linux-amd64
      cp ci-cacher/dist/cacher_linux_arm64_v8.0/cacher  /home/build/pages/cacher-linux-arm64
      cp ci-cacher/dist/cacher_darwin_amd64_v1/cacher   /home/build/pages/cacher-darwin-amd64
      cp ci-cacher/dist/cacher_darwin_arm64_v8.0/cacher /home/build/pages/cacher-darwin-arm64
      chmod +x /home/build/pages/cacher-*
      ( cd /home/build/pages && sha256sum cacher-* > checksums.txt )
      cat /home/build/pages/checksums.txt

      SHA_LA=$( awk '/cacher-linux-amd64$/  {print $1}' /home/build/pages/checksums.txt)
      SHA_LAR=$(awk '/cacher-linux-arm64$/  {print $1}' /home/build/pages/checksums.txt)
      SHA_DA=$( awk '/cacher-darwin-amd64$/ {print $1}' /home/build/pages/checksums.txt)
      SHA_DAR=$(awk '/cacher-darwin-arm64$/ {print $1}' /home/build/pages/checksums.txt)

      # Render CHANGELOG.md to an HTML fragment for inline embedding.
      # Drop the file's top-level `# Changelog` h1 (we already have an
      # <h2>Changelog</h2> in the template) and demote remaining
      # headings one level so they nest under the outer h2.
      cmark --to html ci-cacher/CHANGELOG.md \
        | sed '/<h1>Changelog<\/h1>/d' \
        | sed -e 's|<h3|<h4|g; s|</h3>|</h4>|g; s|<h2|<h3|g; s|</h2>|</h3>|g' \
        > /tmp/changelog.html

      # Substitute scalar placeholders, then insert the changelog at the
      # {{CHANGELOG}} marker via sed's read+delete trick.
      sed -e "s|{{VERSION}}|$VERSION|g" \
          -e "s|{{SHA256}}|$SUM|g" \
          -e "s|{{BUILT}}|$BUILT|g" \
          ci-cacher/docs/index.html > /home/build/dist/index.html
      cd /home/build/dist
          -e "s|{{SHA_LINUX_AMD64}}|$SHA_LA|g" \
          -e "s|{{SHA_LINUX_ARM64}}|$SHA_LAR|g" \
          -e "s|{{SHA_DARWIN_AMD64}}|$SHA_DA|g" \
          -e "s|{{SHA_DARWIN_ARM64}}|$SHA_DAR|g" \
          ci-cacher/docs/index.html \
        | sed -e '/{{CHANGELOG}}/r /tmp/changelog.html' -e '/{{CHANGELOG}}/d' \
        > /home/build/pages/index.html

      cd /home/build/pages
      tar -czvf /home/build/site.tar.gz .
  - publish_pages: |
      hut pages publish -d "$PAGES_DOMAIN" -s "$PAGES_SUBPATH" /home/build/site.tar.gz
  - stage_artifacts: |
      # `artifacts:` paths resolve relative to /home/build. Copy the four
      # binaries + checksums.txt into the top level so they're easy to
      # reference (and to keep the page tarball clean of duplicates).
      cp /home/build/pages/cacher-linux-amd64    /home/build/
      cp /home/build/pages/cacher-linux-arm64    /home/build/
      cp /home/build/pages/cacher-darwin-amd64   /home/build/
      cp /home/build/pages/cacher-darwin-arm64   /home/build/
      cp /home/build/pages/checksums.txt         /home/build/
artifacts:
  - cacher-linux-amd64
  - cacher-linux-arm64
  - cacher-darwin-amd64
  - cacher-darwin-arm64
  - checksums.txt

A .goreleaser.yml => .goreleaser.yml +52 -0
@@ 0,0 1,52 @@
# Goreleaser config for cross-platform cacher builds.
#
# Invoked from .builds/publish.yml as `goreleaser release --clean --skip=publish`
# on tag pushes — we only want the binaries + checksums, not a GitHub
# release (we publish to pages.sr.ht and the build artifacts list).
version: 2

project_name: cacher

before:
  hooks:
    - go mod tidy

builds:
  - id: cacher
    main: .
    binary: cacher
    env:
      - CGO_ENABLED=0
    flags:
      - -trimpath
    ldflags:
      - -s -w
      - -X go.bigb.es/cacher/internal/version.version={{ .Version }}
    goos: [linux, darwin]
    goarch: [amd64, arm64]

archives:
  # Ship raw binaries rather than tarballs so the wget URL
  # `…/cacher-linux-amd64` works directly. The {{ .Version }} prefix lives
  # in the goreleaser dist/ filenames but we strip it when copying into
  # the pages tree (see .builds/publish.yml).
  - id: binary
    formats: [binary]
    name_template: "cacher_{{ .Version }}_{{ .Os }}_{{ .Arch }}"

checksum:
  name_template: "checksums.txt"
  algorithm: sha256

snapshot:
  version_template: "{{ .Tag }}-snapshot"

changelog:
  # We maintain CHANGELOG.md by hand. Don't autogenerate from git log.
  disable: true

release:
  # We publish via pages.sr.ht + build artifacts; there's no GitHub repo to
  # release to. `goreleaser release --skip=publish` already inhibits the
  # release step, but disabling here keeps `goreleaser check` happy.
  disable: true

A CHANGELOG.md => CHANGELOG.md +59 -0
@@ 0,0 1,59 @@
# Changelog

All notable changes to this project will be documented in this file. The format
is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.1.0] — 2026-05-26

First public release. Replaces the `s3_cache_or_curl` /
`s3_cache_docker_image` shell helpers in `tarantool-protobuf/.builds/lib/ci-lib.sh`
with a single static Go binary.

### Added
- `cacher init` / `cacher doctor` — persist config to
  `~/.config/cacher/config.toml` and smoke-test S3 credentials (HEAD bucket
  + 1-byte canary write/read/delete).
- `cacher download` / `upload` / `exists` / `list` / `delete` for single
  files. Download falls back to `--url` on cache miss and back-fills the
  cache. Optional `--sha256` verifies the fetched content.
- `cacher docker {exists,download,upload}` — streamed `docker save | zstd`
  → S3 multipart upload (and inverse). Pure-Go zstd via
  [klauspost/compress](https://github.com/klauspost/compress), no external
  `zstd` binary on the host.
- `cacher dir {download,upload}` — tar+zstd of a directory tree keyed by
  content hash. Closes the gap left by the shell version, which only
  cached single files.
- `cacher key` — resolve a key template (substituting `{hash}`) for shell
  scripting.
- `--hash-from <path>` (repeatable; files or directories) on every command.
  For a single file path the digest exactly matches `sha256sum file | cut -c1-N`,
  so existing keys migrate without recomputation.
- `--arch-suffix` opt-in to suffix every key with `-<goos>-<goarch>`.
- `list --recursive` for flat listing; `list --root` to ignore the
  configured prefix and list at bucket root. Default output style mirrors
  `aws s3 ls` (delimited by `/`).
- Garage-compatible defaults: path-style addressing, signature v4,
  request/response checksum calculation set to `when_required` (Garage
  doesn't implement boto3 1.36+ trailing CRC32 checksums).
- End-to-end test suite (`go test -tags=e2e`) against a real Garage
  container via `testcontainers-go` using `dxflrs/garage:v2.3.0`'s
  `--single-node --default-bucket` mode.
- builds.sr.ht CI: `unit.yml` (every push), `e2e.yml` (every push, with
  Docker), `publish.yml` (tags only, ships cross-platform binaries via
  goreleaser to pages.sr.ht and as build artifacts).
- Static landing page at
  [bigbes.pages.srht.bigb.es/ci-cacher/](https://bigbes.pages.srht.bigb.es/ci-cacher/).

### Fixed
- `doctor` uses ListObjectsV2 (1-key) instead of HeadBucket — Garage
  rejects HeadBucket with 403 even for valid credentials.
- Config path is `~/.config/cacher/config.toml` on every platform
  (previously fell into `~/Library/Application Support/cacher` on macOS
  via `os.UserConfigDir`). cacher is a CI tool; dev-macs and Linux runners
  must look in the same place.

[Unreleased]: https://git.srht.bigb.es/~bigbes/ci-cacher/log/master
[0.1.0]: https://git.srht.bigb.es/~bigbes/ci-cacher/refs/v0.1.0

M docs/index.html => docs/index.html +53 -7
@@ 31,10 31,19 @@
    padding: 0 1.25rem 4rem;
    font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
  }
  h1, h2, h3 { line-height: 1.25; }
  h1, h2, h3, h4 { line-height: 1.25; }
  h1 { font-size: 1.7rem; margin-bottom: 0.15rem; }
  h2 { font-size: 1.15rem; margin-top: 2.2rem; padding-bottom: .2rem; border-bottom: 1px solid var(--rule); }
  h3 { font-size: 1rem; margin-top: 1.4rem; }
  h4 { font-size: .95rem; margin-top: 1rem; color: var(--muted); }
  table { border-collapse: collapse; width: 100%; margin: .7rem 0; font-size: .92em; }
  th, td { text-align: left; padding: .35rem .6rem; border-bottom: 1px solid var(--rule); }
  th { color: var(--muted); font-weight: 600; }
  td code { font-size: .85em; word-break: break-all; }
  .changelog ul { padding-left: 1.25rem; }
  .changelog li { margin: .25rem 0; }
  .changelog h2 { font-size: 1.05rem; margin-top: 1.4rem; }
  .changelog h3 { font-size: .95rem; margin-top: 1rem; color: var(--muted); border-bottom: none; }
  p, ul, ol { margin: .7rem 0; }
  a { color: var(--link); text-decoration: none; }
  a:hover { text-decoration: underline; }


@@ 66,13 75,46 @@
  anywhere you can run a binary and reach an S3 endpoint.
</p>

<h2>Install</h2>
<pre><code>wget https://bigbes.pages.srht.bigb.es/ci-cacher/cacher-linux-amd64 \
  -O ~/.local/bin/cacher
chmod +x ~/.local/bin/cacher</code></pre>
<h2>Download</h2>
<p>
  Pre-built binaries for {{VERSION}}. The shipping URL is stable; the
  SHA-256s below are specific to this build — paste them into your
  pin if you care.
</p>
<table>
  <thead><tr><th>Platform</th><th>Binary</th><th>SHA-256</th></tr></thead>
  <tbody>
    <tr>
      <td>linux/amd64</td>
      <td><a href="cacher-linux-amd64">cacher-linux-amd64</a></td>
      <td><code>{{SHA_LINUX_AMD64}}</code></td>
    </tr>
    <tr>
      <td>linux/arm64</td>
      <td><a href="cacher-linux-arm64">cacher-linux-arm64</a></td>
      <td><code>{{SHA_LINUX_ARM64}}</code></td>
    </tr>
    <tr>
      <td>darwin/amd64</td>
      <td><a href="cacher-darwin-amd64">cacher-darwin-amd64</a></td>
      <td><code>{{SHA_DARWIN_AMD64}}</code></td>
    </tr>
    <tr>
      <td>darwin/arm64</td>
      <td><a href="cacher-darwin-arm64">cacher-darwin-arm64</a></td>
      <td><code>{{SHA_DARWIN_ARM64}}</code></td>
    </tr>
  </tbody>
</table>
<p>
  Verify the binary against the sha256 below before using it.
  All four hashes plus filenames are also available in a single
  <a href="checksums.txt">checksums.txt</a> for piping into <code>sha256sum -c</code>:
</p>
<pre><code>wget https://bigbes.pages.srht.bigb.es/ci-cacher/cacher-linux-amd64 \
  -O ~/.local/bin/cacher
chmod +x ~/.local/bin/cacher
wget -qO- https://bigbes.pages.srht.bigb.es/ci-cacher/checksums.txt \
  | sha256sum -c --ignore-missing</code></pre>

<h2>The loop it replaces</h2>
<pre><code># before — install awscli, write ~/.aws/config, then in every task:


@@ 112,10 154,14 @@ cacher download "$key" "$out" --url "$url"</code></pre>
  the shell version only ever handled single files.
</p>

<h2>Changelog</h2>
<section class="changelog">
{{CHANGELOG}}
</section>

<h2>This build</h2>
<dl class="meta">
  <dt>Version</dt>      <dd>{{VERSION}}</dd>
  <dt>SHA-256</dt>      <dd><code>{{SHA256}}</code></dd>
  <dt>Built</dt>        <dd>{{BUILT}}</dd>
  <dt>Source</dt>       <dd><a href="https://git.srht.bigb.es/~bigbes/ci-cacher">git.srht.bigb.es/~bigbes/ci-cacher</a></dd>
  <dt>License</dt>      <dd>BSD-2-Clause</dd>