diff --git a/.gitea/workflows/bump.yaml b/.gitea/workflows/bump.yaml
index d0a41ab..6288c08 100644
--- a/.gitea/workflows/bump.yaml
+++ b/.gitea/workflows/bump.yaml
@@ -10,21 +10,53 @@ jobs:
container:
image: gentoo/stage3:amd64-openrc
steps:
+ # Cheap gate: compare the newest upstream release against the newest
+ # committed ebuild using only base-image tools (wget). The expensive
+ # toolchain install + bump below is skipped on the common no-op day, so a
+ # daily run that has nothing to do finishes in seconds instead of minutes.
+ # bump-version.sh re-checks this itself and remains the source of truth.
+ - name: Check whether a bump is needed
+ id: check
+ env:
+ BUMP_TOKEN: ${{ secrets.BUMP_TOKEN }}
+ run: |
+ latest=$(wget -qO- https://gitea.com/gitea/runner/releases.rss \
+ | grep -oE '
v[0-9]+\.[0-9]+\.[0-9]+' \
+ | head -1 | sed -E 's#?title>##g; s/^v//')
+ current=$(wget -qO- --header="Authorization: token ${BUMP_TOKEN}" \
+ "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/contents/dev-util/gitea-runner" \
+ | grep -oE '"name": *"gitea-runner-[0-9.]+\.ebuild"' \
+ | sed -E 's#.*gitea-runner-([0-9.]+)\.ebuild.*#\1#' | sort -V | tail -1)
+ echo "upstream=${latest} current=${current}"
+ # Only short-circuit when we are CONFIDENT there is nothing to do
+ # (both versions parsed and current is >= upstream). On any doubt
+ # fall through to the full run and let bump-version.sh decide.
+ if [ -n "$latest" ] && [ -n "$current" ] \
+ && [ "$(printf '%s\n%s\n' "$current" "$latest" | sort -V | tail -1)" = "$current" ]; then
+ echo "needed=false" >> "$GITHUB_OUTPUT"
+ echo "nothing to do (upstream ${latest}, have ${current})"
+ else
+ echo "needed=true" >> "$GITHUB_OUTPUT"
+ fi
+
- name: Sync ::gentoo
+ if: steps.check.outputs.needed == 'true'
run: emerge-webrsync
- name: Configure portage + install tooling
+ if: steps.check.outputs.needed == 'true'
run: |
mkdir -p /etc/portage/repos.conf
printf '[DEFAULT]\nmain-repo = gentoo\n\n[gentoo]\nlocation = /var/db/repos/gentoo\n' \
> /etc/portage/repos.conf/gentoo.conf
- sed -i 's/^verify-signature = true/verify-signature = false/' \
- /etc/portage/binrepos.conf/gentoo.conf
+ sed -i '/^verify-signature/d' /etc/portage/binrepos.conf/gentoo.conf
+ printf '\nverify-signature = false\n' >> /etc/portage/binrepos.conf/gentoo.conf
emerge -q --getbinpkg \
dev-vcs/git net-misc/curl app-arch/xz-utils app-misc/jq \
dev-lang/go dev-util/pkgdev
- name: Check for new release and open PR
+ if: steps.check.outputs.needed == 'true'
env:
BUMP_TOKEN: ${{ secrets.BUMP_TOKEN }}
run: |
diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
index ee20c79..a8f7d8b 100644
--- a/.gitea/workflows/ci.yaml
+++ b/.gitea/workflows/ci.yaml
@@ -2,7 +2,43 @@ name: CI
on: [push]
jobs:
+ # Lint gate: runs on the plain Docker-backend runner (not the Gentoo
+ # container) and must pass before the build job starts. Mirrors the lint job
+ # in the homeserver repo; `just lint` runs markdownlint/shellcheck/yamllint/
+ # actionlint over the repo.
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # setup-just fetches its binary from GitHub; on a Gitea runner the default
+ # github-token is the *Gitea* token (GitHub rejects it 401), so pass none
+ # and use GitHub's unauthenticated API.
+ - uses: extractions/setup-just@v4
+ with:
+ github-token: ""
+
+ - uses: taiki-e/install-action@v2
+ env:
+ GITHUB_TOKEN: ""
+ with:
+ tool: shellcheck
+
+ - name: Install markdownlint-cli
+ run: npm install -g markdownlint-cli
+
+ # actionlint has no maintained setup-action and uv ships via astral.sh;
+ # both installers hit their own release assets (no GitHub token needed).
+ - name: Install yamllint + actionlint tooling
+ run: |
+ curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh
+ bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest /usr/local/bin
+
+ - name: Run linters
+ run: just lint
+
build:
+ needs: lint
# The label only schedules the job on the (Docker-backend) runner; the steps
# actually run inside the Gentoo container declared below.
runs-on: ubuntu-latest
@@ -22,12 +58,16 @@ jobs:
mkdir -p /etc/portage/repos.conf
printf '[DEFAULT]\nmain-repo = gentoo\n\n[gentoo]\nlocation = /var/db/repos/gentoo\n' \
> /etc/portage/repos.conf/gentoo.conf
- sed -i 's/^verify-signature = true/verify-signature = false/' \
- /etc/portage/binrepos.conf/gentoo.conf
+ sed -i '/^verify-signature/d' /etc/portage/binrepos.conf/gentoo.conf
+ printf '\nverify-signature = false\n' >> /etc/portage/binrepos.conf/gentoo.conf
emerge -q --getbinpkg net-misc/curl dev-util/pkgcheck
- # Place this overlay at /var/db/repos/azy5030. actions/checkout is avoided
- # (SHA-256 Gitea repos break it); curl + tar fetches the source archive.
+ # Place this overlay at /var/db/repos/azy5030 via curl + tar. actions/checkout
+ # can't run here: this job uses the gentoo/stage3 container, and the Gitea
+ # runner executes JS actions with `node` *inside* that container
+ # (`docker exec node …`) — stage3 ships no node, so checkout fails with
+ # exit 127. (The lint job uses actions/checkout fine: it has no container:
+ # and runs in the runner's default node-capable image.)
- name: Check out overlay
env:
TOKEN: ${{ github.token }}
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 0000000..dd7faa3
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,9 @@
+{
+ "MD013": false,
+ "MD024": {
+ "siblings_only": true
+ },
+ "MD060": {
+ "style": "compact"
+ }
+}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..2fc6cdb
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,85 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this is
+
+A personal **Gentoo ebuild repository (overlay)**, repo name `azy5030`, EAPI 8,
+`masters = gentoo`, thin + unsigned Manifests (`metadata/layout.conf`). It packages
+software not in the main `::gentoo` tree. Currently one package:
+`dev-util/gitea-runner`. Hosted on a self-hosted Gitea at `git.azy.dev`. (This repo
+is a standard SHA-1 repo; the sibling `homeserver` repo is SHA-256, so workflow
+snippets copied from it may carry a `GIT_DEFAULT_HASH: sha256` checkout override
+that this repo must *not* use.)
+
+## The vendored-build model (the core design)
+
+`dev-util/gitea-runner` is a `go-module` ebuild built **offline** from upstream
+`gitea/runner` source. Gentoo's build sandbox has no network, so Go modules cannot be
+fetched at build time. Instead they are vendored ahead of time:
+
+1. The upstream source archive is `SRC_URI`'d from gitea.com's `/api/v1/.../archive`
+ endpoint (a plain `git clone`/`go get` is never used).
+2. A **vendor tarball** (`${P}-vendor.tar.xz`, the result of `go mod vendor`, packed
+ deterministically) is uploaded as a **release asset on this repo** and is the second
+ `SRC_URI`. `S="${WORKDIR}/runner"`.
+3. `dev-util/gitea-runner/Manifest` pins BLAKE2B/SHA512 of both tarballs. `emerge`
+ verifies against the Manifest, then compiles offline from `vendor/`.
+
+Consequences when editing the ebuild:
+
+- `LICENSE` must cover **every vendored module's** license, not just upstream's MIT.
+ The bump PR checklist suggests `go-licenses report ./...` to confirm.
+- `BDEPEND` Go version tracks upstream's `go.mod` `go` directive.
+- The version string is injected via ldflags into
+ `gitea.com/gitea/runner/internal/pkg/ver.version`; CI asserts `gitea-runner --version`
+ echoes `v${PV}`. If upstream moves that symbol path, the build "succeeds" but reports
+ the wrong version.
+
+## Bumping to a new upstream version
+
+This is **automated** by `scripts/bump-version.sh` (run daily by `bump.yaml`, or
+`workflow_dispatch`). To do it manually you must reproduce its steps, because a version
+bump is never just renaming the ebuild — the vendor tarball must be regenerated and
+re-uploaded as a release asset, or the build will fail Manifest verification. The script:
+
+1. Reads the latest `vX.Y.Z` from upstream `releases.rss`; compares to the newest
+ committed ebuild. Exits early if up to date or if a `bump/gitea-runner-` branch
+ already exists.
+2. Downloads the source archive, runs `go mod vendor`, packs a reproducible
+ `*-vendor.tar.xz` (`--sort=name --mtime='UTC 1970-01-01' --owner=0 --group=0`).
+3. Creates/reuses a release tagged `${PN}-${ver}-vendor` and uploads the tarball asset.
+4. `git mv`s the ebuild to the new version, rewrites `BDEPEND`'s Go version from
+ upstream `go.mod`, and regenerates the Manifest with `pkgdev manifest` (after copying
+ both distfiles into `/var/cache/distfiles` and wiring a temporary `repos.conf`).
+5. Validates: `pkgcheck scan`, then `emerge` + `gitea-runner --version | grep v${ver}`.
+6. Commits, pushes the branch, opens a PR against `master`.
+
+Requires a `BUMP_TOKEN` repo secret (scopes: repository read/write, write release) plus
+a Gentoo env with `go pkgdev git curl xz jq`.
+
+## CI (`.gitea/workflows/ci.yaml`)
+
+Runs on every push, two jobs. A **`lint`** job runs on the plain Docker-backend runner
+(no container), checks out with `actions/checkout`, installs the linters, and runs
+`just lint` (markdownlint/shellcheck/yamllint/actionlint). The **`build`** job has
+`needs: lint` (lint is a gate) and runs inside a `gentoo/stage3:amd64-openrc` container:
+`emerge-webrsync` to sync `::gentoo` → configure portage (disables binpkg signature
+verification so prebuilt deps like `dev-lang/go` are pulled, not compiled) → **check out
+via `curl + tar`, not `actions/checkout`** into `/var/db/repos/azy5030` → `pkgcheck scan`
+→ `emerge` → assert `gitea-runner --version` matches the ebuild version.
+
+The build job can't use `actions/checkout` (even though the lint job does): the Gitea
+runner executes JS actions by `docker exec node …` *inside* the job container, and
+`gentoo/stage3` ships no node, so any JS action fails with exit 127. The lint job has no
+`container:`, so it runs in the runner's default node-capable image and checkout works.
+
+## Conventions / gotchas
+
+- **YAML** is linted by `.yamllint.yaml` (relaxed: line-length and document-start
+ disabled; `truthy.check-keys: false` so `on:` is allowed unquoted).
+- `metadata/md5-cache/` is **gitignored** — portage regenerates it on sync, so never
+ commit it (a stale `gitea-runner-1.0.4` file may linger on disk untracked).
+- The bump script and CI deliberately preserve the scheme of `GITHUB_SERVER_URL`: on the
+ self-hosted runner it is an internal `http://` endpoint. Don't hardcode `https`.
+- New packages must be added to `profiles/categories` (currently just `dev-util`).
diff --git a/Justfile b/Justfile
new file mode 100644
index 0000000..8571d04
--- /dev/null
+++ b/Justfile
@@ -0,0 +1,70 @@
+# azy5030 overlay management commands
+
+# Install dev tooling (linters) used by the lint recipes and CI
+setup-dev:
+ brew install just markdownlint-cli shellcheck actionlint uv
+
+# Lint Markdown: all repo files, or only the .md files in a passed list
+lint-md files="":
+ #!/usr/bin/env bash
+ set -euo pipefail
+ if [ -z "{{files}}" ]; then
+ markdownlint --ignore node_modules '**/*.md'
+ else
+ targets=$(printf '%s\n' {{files}} | grep -E '\.md$' || true)
+ if [ -n "$targets" ]; then markdownlint --ignore node_modules $targets; fi
+ fi
+
+# Lint shell scripts: all repo files, or only the .sh files in a passed list
+lint-sh files="":
+ #!/usr/bin/env bash
+ set -euo pipefail
+ if [ -z "{{files}}" ]; then
+ targets=$(find . -name '*.sh' -not -path './node_modules/*')
+ else
+ targets=$(printf '%s\n' {{files}} | grep -E '\.sh$' || true)
+ fi
+ if [ -n "$targets" ]; then shellcheck $targets; fi
+
+# Lint YAML: all repo files, or only the .yaml/.yml in a passed list (via uv)
+lint-yaml files="":
+ #!/usr/bin/env bash
+ set -euo pipefail
+ if [ -z "{{files}}" ]; then
+ targets=$(find . \( -name '*.yaml' -o -name '*.yml' \) -not -path './node_modules/*')
+ else
+ targets=$(printf '%s\n' {{files}} | grep -E '\.ya?ml$' || true)
+ fi
+ if [ -n "$targets" ]; then uvx yamllint $targets; fi
+
+# Lint Gitea Actions workflows: all, or only the workflow files in a passed list
+lint-actions files="":
+ #!/usr/bin/env bash
+ set -euo pipefail
+ if [ -z "{{files}}" ]; then
+ targets=$(find .gitea/workflows \( -name '*.yaml' -o -name '*.yml' \) 2>/dev/null || true)
+ else
+ targets=$(printf '%s\n' {{files}} | grep -E '^\.gitea/workflows/.*\.ya?ml$' || true)
+ fi
+ # actionlint also runs shellcheck on each `run:` block. No custom runner
+ # labels here (workflows use ubuntu-latest), so no -config-file is needed.
+ if [ -n "$targets" ]; then actionlint $targets; fi
+
+# Autofix all Markdown files
+fix-md:
+ markdownlint --fix --ignore node_modules '**/*.md'
+
+# Apply shellcheck's auto-fixable suggestions to all shell scripts
+fix-sh:
+ #!/usr/bin/env bash
+ set -euo pipefail
+ targets=$(find . -name '*.sh' -not -path './node_modules/*')
+ # shellcheck's diff carries ./ path segments that `git apply` rejects; strip them.
+ diff=$(shellcheck -f diff $targets 2>/dev/null | sed 's|/\./|/|g' || true)
+ if [ -n "$diff" ]; then printf '%s\n' "$diff" | git apply; fi
+
+# Lint everything (all repo files, or only a passed list of files)
+lint files="": (lint-md files) (lint-sh files) (lint-yaml files) (lint-actions files)
+
+# Autofix everything
+fix: fix-md fix-sh