Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,12 @@ jobs:
python-version: '3.12'
- name: Chart lint + render tests
# yq (mikefarah), curl, gpg and Docker ship on ubuntu-latest. The decdn CLI is the
# release the chart pins (appVersion), signature-checked against the vendored
# KEYS, so a decdn bump PR validates the chart against the new release.
# newest release of the line the chart pins (appVersion), resolved through
# GitHub's API (with the job token: runners share the unauthenticated limit) and
# signature-checked against the vendored KEYS, so a decdn bump PR validates the
# chart against the new release.
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
DECDN_CLI="$(scripts/fetch-decdn-cli.sh "$RUNNER_TEMP/decdn")"
export DECDN_CLI
Expand Down
89 changes: 59 additions & 30 deletions .github/workflows/upstream-drift.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
---
name: Upstream drift

# The repo pins one decdn/decdn and one decdn/sponsord release (decdn_node_version,
# sponsord_version) and carries copies of what they publish. This job checks, weekly:
# - the generated mirrors of decdn/decdn at the pinned tag:
# The repo pins one decdn/decdn and one decdn/sponsord release line (decdn_node_version,
# sponsord_version: "X.Y", whose newest finished vX.Y.N the roles install) and carries
# copies of what that release publishes. This job resolves each line as the roles do
# (tasks/resolve-release.yml) and checks, weekly:
# - the generated mirrors of decdn/decdn at that release's tag:
# roles/decdn_node/vars/main/networks.yml (contract addresses) and its subsets in
# roles/sponsord{,_onramp}/vars/main/, and ansible/molecule/schema/files/
# schema-keys.txt (the node.toml key inventory). They describe the binary a
# default deploy installs, so they are generated from the tag, not from main;
# - the hand-copied release facts: the onramp's decdn SHA256SUMS digest (role defaults)
# and the Compose image digests (compose/.env.example), against the pinned
# releases' assets;
# - that each repo's newest vX.Y.Z tag is still the pin.
# - the hand-copied release facts: the onramp's decdn tag and SHA256SUMS digest
# (compose/sponsord-onramp.env.example) and the Compose image digests
# (compose/.env.example), against that release's assets;
# - that each repo's newest vX.Y.Z tag is still in the pinned line.
# It is a scheduled early warning, not a PR gate: an upstream release must not turn
# unrelated PRs red. Fix drift by re-running the generator it names; act on a new
# release with the bump checklist (roles/decdn_node/README.md, roles/sponsord/README.md);
Expand All @@ -38,24 +40,45 @@ jobs:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# The pins, read from the role defaults (plain scalars, so no YAML parser).
- name: Read the pinned releases
# The pins, read from the role defaults (plain scalars, so no YAML parser), and
# each line resolved as the roles resolve it: the highest vX.Y.N that is not a
# draft or a prerelease and has a SHA256SUMS.asc asset. An exact pin is itself.
- name: Read and resolve the pinned releases
id: pins
env:
GH_TOKEN: ${{ github.token }}
run: |
pin() { sed -nE "s/^$2: *\"?([^\" #]*)\"?.*/\\1/p" "ansible/roles/$1/defaults/main.yml"; }
resolve() { # <repo> <pin>: the vX.Y.Z tag a default deploy installs
if [[ "$2" =~ ^[0-9]+\.[0-9]+$ ]]; then
gh api "repos/$1/releases?per_page=100" --jq "
[.[] | select(.draft | not) | select(.prerelease | not)
| select([.assets[].name] | index(\"SHA256SUMS.asc\"))
| .tag_name | capture(\"^v${2//./\\\\.}\\\\.(?<n>0|[1-9][0-9]*)$\")
| .n | tonumber] | max // empty | \"v$2.\\(.)\""
else
echo "v$2"
fi
}
decdn="$(pin decdn_node decdn_node_version)"
sponsord="$(pin sponsord sponsord_version)"
echo "decdn_sums=$(pin sponsord_onramp sponsord_onramp_decdn_sums_sha256)" >> "$GITHUB_OUTPUT"
if [ -z "$decdn" ] || [ -z "$sponsord" ]; then
echo "::error::a role has no pinned version"
exit 1
fi
{ echo "decdn=$decdn"; echo "sponsord=$sponsord"; } >> "$GITHUB_OUTPUT"
# Public repo, the pinned tag. The sync script reads HEAD through git.
decdn_tag="$(resolve decdn/decdn "$decdn")"
sponsord_tag="$(resolve decdn/sponsord "$sponsord")"
if [ -z "$decdn_tag" ] || [ -z "$sponsord_tag" ]; then
echo "::error::no finished release in decdn/decdn $decdn or decdn/sponsord $sponsord"
exit 1
fi
{ echo "decdn=$decdn"; echo "sponsord=$sponsord"
echo "decdn_tag=$decdn_tag"; echo "sponsord_tag=$sponsord_tag"; } >> "$GITHUB_OUTPUT"
# Public repo, the resolved tag. The sync script reads HEAD through git.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: decdn/decdn
ref: v${{ steps.pins.outputs.decdn }}
ref: ${{ steps.pins.outputs.decdn_tag }}
path: upstream
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
Expand All @@ -66,9 +89,10 @@ jobs:
# upstream change. Full output goes to the job summary; annotations stay one line.
- name: Compare the mirrors and the pins with upstream
env:
DECDN_PIN: v${{ steps.pins.outputs.decdn }}
SPONSORD_PIN: v${{ steps.pins.outputs.sponsord }}
DECDN_SUMS: ${{ steps.pins.outputs.decdn_sums }}
DECDN_PIN: ${{ steps.pins.outputs.decdn }}
SPONSORD_PIN: ${{ steps.pins.outputs.sponsord }}
DECDN_TAG: ${{ steps.pins.outputs.decdn_tag }}
SPONSORD_TAG: ${{ steps.pins.outputs.sponsord_tag }}
run: |
rc=0
check() {
Expand All @@ -93,32 +117,37 @@ jobs:
python3 ansible/molecule/schema/files/gen-schema-keys.py upstream > "$fresh" || return 2
diff -u ansible/molecule/schema/files/schema-keys.txt "$fresh"
}
# Exit 1 when the repo's newest vX.Y.Z tag (pre-releases skipped) is not the pin.
# Exit 1 when the repo's newest vX.Y.Z tag (pre-releases skipped) is not in the
# pinned line (or, for an exact pin, is not the pin): a new line is a bump.
# shellcheck disable=SC2329 # invoked indirectly, through check()
release_check() {
local repo="$1" pin="$2" tags latest
tags="$(git ls-remote --tags --refs "https://github.com/$repo" 'v*')" || return 2
latest="$(sed -n 's#.*refs/tags/##p' <<<"$tags" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V | tail -n 1)" || true
[ -n "$latest" ] || { echo "$repo: no vX.Y.Z tag found"; return 2; }
[ "$latest" = "$pin" ] && return 0
[ "$latest" = "v$pin" ] || [ "${latest%.*}" = "v$pin" ] && return 0
echo "$repo: pinned $pin, newest vX.Y.Z tag $latest"
return 1
}
# shellcheck disable=SC2329 # invoked by sums_check and digest_check
asset() { # <repo> <tag> <file>: a release asset on stdout
curl -fsSL --retry 3 "https://github.com/$1/releases/download/$2/$3"
}
# Exit 1 when a pinned SHA256SUMS digest is not the release's. The installers
# the onramp serves check the downloaded SHA256SUMS against it, so a stale
# one would only fail on users' machines.
# Exit 1 when Compose's onramp installer pin (compose/sponsord-onramp.env.example)
# is not the release a default deploy serves, or its SHA256SUMS digest is not
# that release's. The installers check the downloaded SHA256SUMS against it,
# so a stale one would only fail on users' machines.
# shellcheck disable=SC2329 # invoked indirectly, through check()
sums_check() {
local repo="$1" tag="$2" want="$3" got
local repo="$1" tag="$2" env=compose/sponsord-onramp.env.example release want got
release="$(sed -nE 's/^ONRAMP_DECDN_RELEASE=//p' "$env")"
want="$(sed -nE 's/^ONRAMP_DECDN_SUMS_SHA256=//p' "$env")"
[ "$release" = "$tag" ] || { echo "$env: ONRAMP_DECDN_RELEASE is $release, not $tag"; return 1; }
got="$(asset "$repo" "$tag" SHA256SUMS | sha256sum)" || return 2
got="${got%% *}"
[ "$got" = "$want" ] && return 0
echo "$repo $tag: SHA256SUMS is $got, the role pins $want"
echo "$repo $tag: SHA256SUMS is $got, $env pins $want"
return 1
}
# Exit 1 when a compose/.env.example digest is not the one the pinned release's
Expand All @@ -136,16 +165,16 @@ jobs:
echo "$repo $tag: $var should be $got, compose/.env.example has $want"
return 1
}
echo "### Upstream drift (decdn/decdn $DECDN_PIN, $(git -C upstream rev-parse --short HEAD))" >> "$GITHUB_STEP_SUMMARY"
echo "### Upstream drift (decdn/decdn $DECDN_PIN: $DECDN_TAG, $(git -C upstream rev-parse --short HEAD); decdn/sponsord $SPONSORD_PIN: $SPONSORD_TAG)" >> "$GITHUB_STEP_SUMMARY"
check "network profiles" python3 scripts/sync-network-profiles.py upstream --ref HEAD --check
check "config schema keys" schema_check
check "decdn/decdn release" release_check decdn/decdn "$DECDN_PIN"
check "decdn/sponsord release" release_check decdn/sponsord "$SPONSORD_PIN"
check "onramp decdn SHA256SUMS digest" sums_check decdn/decdn "$DECDN_PIN" "$DECDN_SUMS"
check "Compose decdn-node digest" digest_check decdn/decdn "$DECDN_PIN" DECDN_IMAGE_DIGEST \
check "decdn/decdn release line" release_check decdn/decdn "$DECDN_PIN"
check "decdn/sponsord release line" release_check decdn/sponsord "$SPONSORD_PIN"
check "Compose onramp decdn pin" sums_check decdn/decdn "$DECDN_TAG"
check "Compose decdn-node digest" digest_check decdn/decdn "$DECDN_TAG" DECDN_IMAGE_DIGEST \
decdn-node-image-digest.txt
check "Compose sponsord digest" digest_check decdn/sponsord "$SPONSORD_PIN" SPONSORD_IMAGE_DIGEST \
check "Compose sponsord digest" digest_check decdn/sponsord "$SPONSORD_TAG" SPONSORD_IMAGE_DIGEST \
sponsord-image-digest.txt
check "Compose sponsord-onramp digest" digest_check decdn/sponsord "$SPONSORD_PIN" \
check "Compose sponsord-onramp digest" digest_check decdn/sponsord "$SPONSORD_TAG" \
SPONSORD_ONRAMP_IMAGE_DIGEST sponsord-onramp-image-digest.txt
exit "$rc"
69 changes: 41 additions & 28 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ scripts/ # upstream-mirror generators, the compose Alloy config g
`ansible/roles/sponsord/vars/main/networks.yml` and
`ansible/roles/sponsord_onramp/vars/main/networks.yml` (all `scripts/sync-network-profiles.py`)
and `ansible/molecule/schema/files/schema-keys.txt` (`gen-schema-keys.py`), both
generated from the decdn/decdn tag `decdn_node_version` pins. The weekly
`upstream-drift` workflow flags staleness against that tag, and a newer upstream
release of either repo. These are the only protocol facts (contract addresses) the
generated from the tag of the newest decdn/decdn release in the line
`decdn_node_version` pins. The weekly `upstream-drift` workflow flags staleness
against that tag, and a new upstream release line of either repo. These are the only protocol facts (contract addresses) the
repo carries, and `networks.yml` carries its upstream commit.

**Generated from this repo — regenerate, never hand-edit:** `compose/alloy/config.alloy`
Expand All @@ -141,23 +141,33 @@ runtime (`scripts/render-compose-alloy.sh`, which runs the `compose`-tagged task
test-scripts` fails while the committed file differs from a fresh render, and `make
lint-alloy` validates and runs it.

**Pinned upstream releases.** One decdn/decdn release (`decdn_node_version`, today
`0.1.0`) and one decdn/sponsord release (`sponsord_version` = `sponsord_onramp_version`,
today `0.1.0`; decdn/sponsord tags its whole workspace `vX.Y.Z`, there are no per-crate
tags) are pinned across every path: the role defaults, the onramp's installer pin
(`sponsord_onramp_decdn_release` + `_decdn_sums_sha256`, copied into
`compose/sponsord-onramp.env.example`), `compose/.env.example`'s image digests, the
chart's `appVersion` and the generated mirrors. cloud-init takes the role defaults at
`DEVOPS_REF`. Docs name the pin (`decdn_node_version`, `sponsord_version`), never its
number: besides this paragraph, only the commented examples and the comments beside a
pinned value name a number, and they move with the pin (`bump_decdn.py` moves decdn's;
the sponsord checklist lists sponsord's). `make test-scripts` checks the pins agree
with each other, the Compose example, `appVersion`, the chart's `artifacthub.io/images`
tag and this paragraph, and that both vendored KEYS hold the keys SECURITY.md
publishes; the weekly `upstream-drift` job checks the digests and the mirrors against
the releases and flags a newer upstream tag. The `--version` check compares the pin in
`release` mode only (exactly, on `<binary> <version>`). Bump everything together with
the checklists in `roles/decdn_node/README.md` and `roles/sponsord/README.md`;
**Pinned upstream releases.** One decdn/decdn release line (`decdn_node_version`, today
`0.1`) and one decdn/sponsord release line (`sponsord_version` = `sponsord_onramp_version`,
today `0.1`; decdn/sponsord tags its whole workspace `vX.Y.Z`, there are no per-crate
tags) are pinned across every path. A line, `X.Y`, is what the role defaults, the
onramp's installer pin (`sponsord_onramp_decdn_release`, `vX.Y`) and the chart's
`appVersion` name: the roles install its newest finished `vX.Y.N` on every run
(`tasks/resolve-release.yml`, copied into all three roles: GitHub's release list, then
the same signed `SHA256SUMS` and exact `--version` check against the resolved version;
the onramp reads the installers' digest from that release's verified `SHA256SUMS`),
and the chart's default image tag is the line's floating tag, pulled `Always`. An
exact `X.Y.Z` in inventory (or `image.tag`/`image.digest`) holds a host on one release.
So an upstream patch release needs no release of the collections or the chart; a new
line does. What cannot resolve a line names one release of it and moves on every
upstream release, repo-only: `compose/.env.example`'s image digests, the onramp's
installer pin in `compose/sponsord-onramp.env.example` (tag + `SHA256SUMS` digest), the
chart's example digest, the commented exact-pin examples, and the generated mirrors
(which a patch is not expected to change; when one does, the bump PR says so and the
collections need a release). cloud-init takes the role defaults at `DEVOPS_REF`. Docs
name the pin (`decdn_node_version`, `sponsord_version`), never its number: besides this
paragraph, only the commented examples and the comments beside a pinned value name a
number, and they move with the pin (`bump_decdn.py` moves decdn's; the sponsord
checklist lists sponsord's). `make test-scripts` checks the pins agree with each other,
the Compose example (a release of the line), `appVersion`, the chart's
`artifacthub.io/images` tag and this paragraph, and that the vendored KEYS hold the
keys SECURITY.md publishes; the weekly `upstream-drift` job checks the exact copies and
the mirrors against each line's newest release and flags a newer upstream line. Bump
with the checklists in `roles/decdn_node/README.md` and `roles/sponsord/README.md`;
`scripts/open-decdn-bump.sh` opens the decdn bump's PR (decdn's `publish-crates.sh`
runs it after each release). Both upstream repos are public and also publish their
binaries as crates (`decdn-node`, `decdn-cli`, `sponsord`, `sponsord-onramp`); the
Expand Down Expand Up @@ -199,8 +209,9 @@ the pinned `iroh-relay` (re-capture it when bumping `iroh_relay_version`).
backend holds (`relay_foreign_namespaces`, ADR 002 § Retrieval by namespace, ADR 037). Recognition as an origin is on-chain (`OriginAssignment.addOrigin`,
the publisher's step). **The public deCDN node**
(baseline + `decdn-node`), installed by `decdn_node_install_method`:
`release` (the default) — a pinned GitHub release tarball verified against the
release's GPG-signed `SHA256SUMS` (`decdn_node_version`); `source` — a git ref (any tag/branch/SHA) cloned and `cargo build`-ed **on the
`release` (the default) — a GitHub release tarball, the newest of the pinned line or
an exact release (`decdn_node_version`, `tasks/resolve-release.yml`), verified
against the release's GPG-signed `SHA256SUMS`; `source` — a git ref (any tag/branch/SHA) cloned and `cargo build`-ed **on the
node** as the unprivileged `decdn-build` user, sha256-pinned rustup, a `<repo>@<commit>`
stamp (`tasks/source.yml`, copied into both sponsord roles, which share the user,
home and toolchain). **No build can poison a later one:** root owns the home, the
Expand Down Expand Up @@ -297,7 +308,8 @@ the pinned `iroh-relay` (re-capture it when bumping `iroh_relay_version`).
imported by `publisher.yml`) targets its own `sponsord_hosts` group, standalone or
co-located with a node.
- **Install:** a GPG-verified `v*` decdn/sponsord release by default
(`sponsord_version`), a host-side `source` build, or a manual binary.
(`sponsord_version`: the newest of its line, or an exact release), a host-side
`source` build, or a manual binary.
The `--version` backstop checks the version in `release` mode only (the version
has a default, so other modes would fail against it), as in the other two roles.
- **Unit:** `DynamicUser`; the API token (generated on the host, never replaced),
Expand Down Expand Up @@ -647,8 +659,9 @@ the pinned `iroh-relay` (re-capture it when bumping `iroh_relay_version`).

- **`charts/decdn-node/`** — the same node on Kubernetes: a one-replica StatefulSet (one
release = one identity) on the upstream `decdn-node` image (`ghcr.io/decdn/decdn-node`,
tag `appVersion` = the role's `decdn_node_version` unless `image.tag`/`image.digest`
is set), PVC data dir, a `prepare` init
tag `appVersion` = the role's `decdn_node_version` line, the floating tag upstream
moves to each release in it, pulled `Always` (`decdn-node.pullPolicy`), unless
`image.tag`/`image.digest` is set), PVC data dir, a `prepare` init
container that installs the identity files from an `existingSecret` onto the PVC at
`0600` (upstream rejects symlinked or group/world-readable key files) and the password
into an in-memory volume, `DECDN_RPC_URL` via `secretKeyRef` (named keys only), public
Expand All @@ -662,9 +675,9 @@ the pinned `iroh-relay` (re-capture it when bumping `iroh_relay_version`).
above applies here too:** `make lint-helm` runs the same `check-schema-keys.py` +
`schema-keys.txt` on the rendered ConfigMap, so a re-sync covers both paths. CI's
`helm` job also runs the real `decdn config validate` on each `ci/*.yaml` render, against the
`decdn` of the release `appVersion` pins (`scripts/fetch-decdn-cli.sh`: the role's
GPG check against the vendored KEYS), so a decdn bump PR validates the chart against
the new release. Optional
newest `decdn` of the line `appVersion` pins (`scripts/fetch-decdn-cli.sh`: the
role's line resolution and GPG check against the vendored KEYS), so a decdn bump PR
validates the chart against the new release. Optional
`PrometheusRule` + dashboard ConfigMaps render `files/monitoring/`, the symlink to
`monitoring/decdn-node/` (never through `tpl`:
the alert annotations carry Prometheus templates).
Expand Down
Loading
Loading