~bigbes/sr-ht-spec

ef7bddf8b3d64cb4c204a064ad73dbacc78443cb — Eugene Blikh 3 days ago 6e59bf0
ci: publish the apk into artifacts.sr.ht as well

The abuild output goes on to the S3 bucket phoebe re-indexes, and now also to
the ~bigbes/main channel of artifacts.sr.ht, which indexes and signs it in the
same request. One PUT per file with the shared working token; a 409 says the
job ran twice and is a warning rather than a failure.
2 files changed, 109 insertions(+), 1 deletions(-)

M .build.yml
M docs/ci.md
M .build.yml => .build.yml +51 -0
@@ 26,6 26,9 @@ secrets:
  # same pair the bencher and ci-cacher builds use.
  - 7dde4219-0783-4581-a67d-c94749de3600   # ~/.s3-cache-key-id
  - 0e5b3530-6f19-4f30-9b73-9339dd382e46   # ~/.s3-cache-key-secret
  # A tokens.sr.ht working token carrying artifacts:upload, the same secret the
  # sibling services mount. It is what publish_artifacts sends.
  - c7968415-1a6d-4ca0-a188-150fb7f57b65   # ~/.srht-token
sources:
  - https://git.srht.bigb.es/~bigbes/sr-ht-spec
environment:


@@ 33,6 36,12 @@ environment:
  APK_REPO: alpine/v3.22/bigbes/x86_64
  S3_BUCKET: repo
  S3_ENDPOINT: https://s3.bigb.es
  # The second destination of the same apk: one channel of artifacts.sr.ht
  # holds the packages of every sibling service, so a consumer adds one
  # repository line. See docs/ci.md#publish_artifacts.
  ARTIFACTS_ENDPOINT: https://artifacts.srht.bigb.es
  ARTIFACTS_CHANNEL: "~bigbes/main"
  ARTIFACTS_DIST: v3.22
  # Must track the srht deployment's SRHT_CORE_VER, or this service's theme
  # drifts from the rest of the instance. BOOTSTRAP_REV is the submodule commit
  # core.sr.ht pins at that tag; bump the two together.


@@ 199,6 208,48 @@ tasks:
        echo "uploaded $(basename "$f")"
      done
      echo "published; apk-mirror on phoebe re-indexes within 15 minutes"
  - publish_artifacts: |
      # The same apk into the artifacts.sr.ht channel, beside the S3 copy above.
      # The two destinations are independent on purpose: S3 is the road phoebe
      # still walks, this one is the road being opened. Its failure is its own,
      # and it never rolls back the upload that already succeeded.
      # See docs/ci.md#publish_artifacts.
      if [ ! -r ~/.srht-token ]; then
        echo "no ~/.srht-token: nothing was published to artifacts.sr.ht"
        exit 0
      fi
      count=$(find "$HOME/packages" -name '*.apk' | wc -l)
      test "$count" -gt 0 || { echo "no .apk under $HOME/packages" >&2; exit 1; }
      url="$ARTIFACTS_ENDPOINT/api/v1/pkg/$ARTIFACTS_CHANNEL/apk/$ARTIFACTS_DIST"
      # A `find | while read` would run the body in a subshell under ash and
      # lose $failed with it, so the loop reads a word list instead.
      failed=
      for file in $(find "$HOME/packages" -name '*.apk'); do
        set +x   # the token must not reach the log
        code=$(curl -sS -o /tmp/artifacts.out -w '%{http_code}' -X PUT \
          -H "Authorization: Bearer $(cat ~/.srht-token)" \
          --data-binary "@$file" "$url")
        set -x
        case "$code" in
        200|201)
          echo "published $(basename "$file") -> $code"
          ;;
        409)
          # Same version, other bytes. abuild stamps mtimes into the archive,
          # so resubmitting one commit builds a byte-different apk under the
          # same pkgver; the published copy stands and the build stays green.
          echo "WARNING: $(basename "$file") already published with other bytes; kept the published copy"
          ;;
        *)
          echo "FAILED $(basename "$file") -> $code" >&2
          cat /tmp/artifacts.out >&2
          echo >&2
          failed=1
          ;;
        esac
      done
      test -z "$failed" || exit 1
      echo "index: $ARTIFACTS_ENDPOINT/$ARTIFACTS_CHANNEL/apk/$ARTIFACTS_DIST/x86_64/APKINDEX.tar.gz"
  - cache_save: |
      # AFTER publish so an S3 hiccup cannot strand a good apk, and fatal on
      # purpose. Without --force an upload skips a key already there, so no

M docs/ci.md => docs/ci.md +58 -1
@@ 30,13 30,14 @@ so nothing at runtime needs either.

## secrets

Three, all account-level and shared with the sibling services:
Four, all account-level and shared with the sibling services:

| secret | lands at | used by |
|---|---|---|
| `apk-ci-s3` | `~/.apk-ci.env` | `publish` |
| `7dde4219-…` | `~/.s3-cache-key-id` | `cacher_init` |
| `0e5b3530-…` | `~/.s3-cache-key-secret` | `cacher_init` |
| `c7968415-…` | `~/.srht-token` | `publish_artifacts` |

They are file secrets. Listing them is what turns `publish` and the cache tasks
on; a manual submission that asks for no secrets still runs the interesting part


@@ 273,6 274,62 @@ previously published version of every service on the instance.

`apk-mirror` on phoebe re-indexes within 15 minutes; nothing here waits for it.

## publish_artifacts

The same apk, uploaded a second time — into the `~bigbes/main` channel of
`artifacts.sr.ht`, which indexes and signs it in the same request rather than on
a 15-minute timer. One `PUT` per file to
`/api/v1/pkg/~bigbes/main/apk/v3.22`, with the working token from
`~/.srht-token` as the bearer. No client binary is involved: the upload route
takes the file as the request body, and `curl` is already installed for the
cacher.

It is a task of its own and not two more lines inside `publish` for one reason:
the S3 copy is what phoebe re-indexes today and this one is the road being
opened. A refusal here has to be legible as *this* destination refusing, and it
must not be able to undo an upload that already succeeded.

`set +x` around every `curl`, for the same reason `publish` has it: the token is
on the command line and the log is world-readable.

Three answers are expected and only two of them are good:

- `201` — published, indexed, signed.
- `200` — the same version with the same bytes was already there. A rerun of a
  job that got as far as this task is idempotent, and stays green.
- `409` — the same version with *different* bytes. `abuild` stamps mtimes into
  the archive, so resubmitting one commit produces a byte-different apk under an
  identical `pkgver`; a published version is immutable, so the daemon keeps what
  it has. That is a `WARNING` and not a failure — it means the build was run
  twice, not that anything is wrong. Republishing means removing the version
  through the API first.

Anything else prints the daemon's error body and fails the task, after trying
every remaining file: one bad package must not hide the fate of the others.

The loop is `for file in $(find …)` rather than `find … | while read`, because
under ash the second form runs its body in a subshell and the failure flag set
in it is lost when the pipeline ends — the task would report success it never
had.

Gated on `~/.srht-token` being readable, exactly as `publish` is gated on
`~/.apk-ci.env`: a manual submission without secrets still builds the apk, and
says that nothing was published.

Installing what it publishes, on any Alpine box:

```sh
wget -qO /etc/apk/keys/bigbes@artifacts.srht.bigb.es.rsa.pub \
  https://artifacts.srht.bigb.es/~bigbes/keys/apk.rsa.pub
echo https://artifacts.srht.bigb.es/~bigbes/main/apk/v3.22 >> /etc/apk/repositories
apk update && apk add spec.sr.ht
```

The name of the key file is not free: apk looks a signature up by the name
carried in the index's `.SIGN.RSA256.<name>` entry, so it has to be
`bigbes@artifacts.srht.bigb.es.rsa.pub` and nothing else. With the key in place
no `--allow-untrusted` is needed — the daemon signs the index it builds.

## cache_save

After `publish`, deliberately: a cache upload that fails must not strand an apk