Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8b687f4e4 | ||
|
|
06018fdda4 | ||
|
|
02159d95e2 | ||
|
|
51dc5ddf5c | ||
|
|
affb2ac903 | ||
|
|
dfdc720434 | ||
|
|
34f8fb85c9 | ||
|
|
4395887fa9 | ||
|
|
44a74ce187 | ||
|
|
f6e2480d37 | ||
|
|
ab362e8d75 | ||
|
|
a808447747 | ||
|
|
c6afe46220 | ||
|
|
11b1d7f726 | ||
|
|
4289bb59d8 | ||
|
|
e8a80ed3e8 | ||
|
|
361a7413fe | ||
|
|
9586a464ea | ||
|
|
3bdc5a664b | ||
|
|
f86ceb9441 | ||
|
|
8b7b9cfff0 | ||
|
|
6c5f652375 | ||
|
|
ae91766acc | ||
|
|
cfc1b01b1f | ||
|
|
078b0d814b | ||
|
|
d8924693d5 | ||
|
|
bd590edfea | ||
|
|
1436bb2614 | ||
|
|
9c1735de00 | ||
|
|
16025633a1 | ||
|
|
1005257774 | ||
|
|
110925c44a | ||
|
|
c7797d05ac | ||
|
|
e04131064c | ||
|
|
c57c6ba778 | ||
|
|
1e6711d1b2 | ||
|
|
8d61332742 | ||
|
|
d356191859 | ||
|
|
86e1a5be71 | ||
|
|
fce3dbc2ac |
@@ -89,6 +89,7 @@ jobs:
|
||||
- providers
|
||||
- conversation
|
||||
- share
|
||||
- mirror
|
||||
name: confined (${{ matrix.suite }})
|
||||
container:
|
||||
# Debian bookworm plus a node binary - node is what actions/checkout
|
||||
|
||||
+39
-4
@@ -6,7 +6,7 @@ What each release changed, newest first, for one question: **should I update,
|
||||
and what will I be getting.** `dejima --version` says which release the
|
||||
checkout you are running is on.
|
||||
|
||||
A release is a date - `2026.10.13` - and the git tag of that name on `main` is
|
||||
A release is a date - `2026.10.14` - and the git tag of that name on `main` is
|
||||
where it lives. A date rather than a version number because the useful fact
|
||||
about a release is how old it is, and because `MAJOR.MINOR` would be a promise
|
||||
about compatibility and patch lines that this project does not make.
|
||||
@@ -22,9 +22,44 @@ requests, which are linked.
|
||||
|
||||
## Unreleased
|
||||
|
||||
Nothing yet. The release pull request that merges `development` into `main`
|
||||
replaces this heading with the date it merges, and tags `main` with the same
|
||||
date.
|
||||
The release pull request that merges `development` into `main` replaces this
|
||||
heading with the date it merges, and tags `main` with the same date.
|
||||
|
||||
### Changed
|
||||
|
||||
- **The GitHub mirrors carry a rewritten history.** AI co-author trailers are
|
||||
removed from commit messages, so GitHub credits no tool as a contributor,
|
||||
and operator-specific values are replaced throughout. Commits keep their
|
||||
authors and dates, but a forge SHA no longer resolves on GitHub.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The credential proxy refuses an oversized request body before inviting
|
||||
it.** It answered `100 Continue` and only then read the declared length, so
|
||||
whether a session saw the `413` or only the `100` was timing. The cap itself
|
||||
always held - nothing oversized was read into host memory - but the
|
||||
conversation was a race, and it surfaced as an intermittently red `proxy`
|
||||
job.
|
||||
([#400](https://gitbox.branderson.io/dejima/dejima/issues/400))
|
||||
|
||||
## 2026.10.14
|
||||
|
||||
Nothing a session does has changed. This release is about where dejima can be
|
||||
read.
|
||||
|
||||
### New
|
||||
|
||||
- **Read-only mirrors on GitHub.** dejima, dejima-badge and the three sister
|
||||
projects are published to [github.com/dejima-org](https://github.com/dejima-org):
|
||||
`main` and its release tags, synced hourly. Development, issues and pull
|
||||
requests stay on the forge, and the mirrors accept neither. A clone of a
|
||||
mirror has GitHub links and clone URLs where the forge's copy has the forge's.
|
||||
([#406](https://gitbox.branderson.io/dejima/dejima/pulls/406))
|
||||
|
||||
### Rollback
|
||||
|
||||
This release adds no config key and changes nothing a session runs, so rolling
|
||||
back changes nothing an operator would notice.
|
||||
|
||||
## 2026.10.13
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ anonymous issues or pull requests. So there is currently no way for you to
|
||||
file either, and that is a property of where this lives rather than a comment
|
||||
on the contribution.
|
||||
|
||||
The repositories under [github.com/dejima-org](https://github.com/dejima-org)
|
||||
are read-only mirrors of `main`, with issues and pull requests turned off.
|
||||
|
||||
That will change: this project will provide a way to submit issues and pull
|
||||
requests. Until it does, if you are using dejima and need it to do something
|
||||
different, **fork it and make the change in your fork**. Nothing here depends
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
browser is set - so anything conditional shows dark ink on a dark page to
|
||||
a reader whose browser prefers light, and does it silently.
|
||||
The suffix names the mode a file is FOR, not the colour of its ink:
|
||||
-darkmode is the pale wordmark that belongs on a dark page. A mirror on a
|
||||
light-themed host swaps the filename below for dejima-wordmark-lightmode.png,
|
||||
which is committed alongside for exactly that. One line, on the mirror,
|
||||
where someone can see whether it worked.
|
||||
-darkmode is the pale wordmark that belongs on a dark page. GitHub does
|
||||
follow its own theme setting through <picture>, so the GitHub mirror
|
||||
replaces the tag below with one that falls back to
|
||||
dejima-wordmark-lightmode.png; see mirror/github-mirror.sh.
|
||||
Every here is spacing, and it has to be a character rather than CSS
|
||||
or a presentation attribute. Gitea's sanitizer strips `style` from the tag
|
||||
and from any wrapper, `hspace`/`vspace` survive sanitizing only to be
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@ page that renders dark, `-lightmode` for one that renders light. The suffix
|
||||
names the mode a file is for, not the colour of its ink, so `-darkmode` is the
|
||||
pale one.
|
||||
|
||||
The README points at `-darkmode` unconditionally. A mirror on a light-themed
|
||||
host changes that one line; the comment above the tag says why it is not
|
||||
conditional.
|
||||
The README points at `-darkmode` unconditionally; the comment above the tag
|
||||
says why it is not conditional. The GitHub mirror rewrites that tag to pick
|
||||
between the two by the reader's theme.
|
||||
|
||||
**These are trademarks and are not covered by this repository's GPL licence.**
|
||||
Who holds them, and what may be done with them, is stated once for the whole
|
||||
|
||||
+4
-1
@@ -974,7 +974,10 @@ tcb_snapshot() {
|
||||
# backends/ likewise: a backend.conf names the upstream every request
|
||||
# is forwarded to and the host path the credential is read from, so an
|
||||
# edit here redirects where a real credential is sent.
|
||||
for dir in bin hooks docker test forges workflows harnesses tools backends; do
|
||||
#
|
||||
# mirror/ runs on the host from a timer, holding a key that can push
|
||||
# to every repository in the GitHub organisation.
|
||||
for dir in bin hooks docker test forges workflows harnesses tools backends mirror; do
|
||||
if [ -d "$ws/$dir" ]; then
|
||||
while IFS= read -r -d '' f; do
|
||||
stage_entry "tcb:${f#"$ws"/}" "$f"
|
||||
|
||||
@@ -424,6 +424,33 @@ class Handler(BaseHTTPRequestHandler):
|
||||
% (what, n, self.MAX_BODY_BYTES))
|
||||
return n
|
||||
|
||||
def handle_expect_100(self):
|
||||
"""Refuse an oversized body before inviting it.
|
||||
|
||||
The default answers `100 Continue` and returns True, and it runs
|
||||
before do_* - so the proxy invited the upload, read the length in
|
||||
_read_body, refused it and closed mid-flight. Whether the client saw
|
||||
the 413 or only the 100 was timing, which is a race a client cannot
|
||||
be expected to win and a test cannot be expected to pass reliably.
|
||||
|
||||
The declared length is all there is to go on before a body arrives.
|
||||
A chunked request declares nothing and is capped as it is read, which
|
||||
is the one case where the conversation still ends mid-upload - there
|
||||
is no earlier moment to end it at.
|
||||
"""
|
||||
try:
|
||||
self._length(self.headers.get("Content-Length", 0), "Content-Length")
|
||||
except BodyTooLarge as e:
|
||||
# Closed for the same reason the other 413 closes: a client that
|
||||
# sends anyway - curl gives up waiting after a second - leaves its
|
||||
# bytes queued, and the next parse would read them as a request
|
||||
# line and answer with garbage 400s instead of one clean refusal.
|
||||
self.close_connection = True
|
||||
self._fail(413, "credential proxy: request body too large",
|
||||
log_message="refused a request body: %s" % e)
|
||||
return False
|
||||
return super().handle_expect_100()
|
||||
|
||||
def _read_body(self):
|
||||
if self.headers.get("Transfer-Encoding", "").lower() == "chunked":
|
||||
chunks = []
|
||||
|
||||
@@ -160,3 +160,31 @@ getting - so it names what an operator would notice rather than what changed.
|
||||
The pull requests carry the detail and are linked from it. A tag is a record
|
||||
of what shipped, not a version anyone may stay on; `CHANGELOG.md` says so at
|
||||
the top, and `SECURITY.md` is where that is binding.
|
||||
|
||||
## 7. The GitHub mirror
|
||||
|
||||
`mirror/github-mirror.sh` publishes `main` and the tags on it, for this
|
||||
repository and every other one in the organisation, to
|
||||
[github.com/dejima-org](https://github.com/dejima-org) from the operator's
|
||||
host, hourly, so a release needs no step here. Issues, pull requests and
|
||||
Actions are off there; all of that stays on the forge. A new repository is
|
||||
mirrored once it is added to `MIRROR_REPOS` in the script.
|
||||
|
||||
Each mirror's history is the forge's `main` with AI co-author trailers taken
|
||||
out of the commit messages, so GitHub credits no tool as a contributor, and
|
||||
with the values in the operator's scrub file replaced throughout. That file
|
||||
stays on the host, because the list is the thing it hides. The commits keep
|
||||
their authors and dates but not their SHAs, so a forge SHA does not resolve on
|
||||
GitHub. On top sits one commit that points repository links and clone URLs at
|
||||
GitHub, lets the README wordmark follow the reader's
|
||||
theme, and says at the top of the README that it is a mirror. A README change
|
||||
that breaks one of those rewrites stops that repository's mirror rather than
|
||||
publishing the forge's version. Here, `test/mirror.bats` fails on it first; in
|
||||
the other repositories, the hourly run fails and names it.
|
||||
|
||||
Setup is in the header of `mirror/dejima-github-mirror.service`. Once the
|
||||
GitHub repositories exist, `github-mirror.sh --settings` turns their features
|
||||
off, and `--dry-run` builds what a sync would publish without pushing it. A
|
||||
sync refuses a mirror whose `main` is not built on the history it would
|
||||
publish, which happens when the rewrite rules change or the scrub file gains a
|
||||
value; `--reset` force-pushes over it.
|
||||
|
||||
+3
-1
@@ -280,7 +280,9 @@ layout it is unpacked with, all consumed by the image build - so a session
|
||||
that edits one chooses what the next launch downloads and installs as root.
|
||||
`backends/*/backend.conf` names the upstream every request is forwarded to and
|
||||
the host path the credential is read from, so an edit there redirects where a
|
||||
real credential is sent.
|
||||
real credential is sent. `mirror/github-mirror.sh` runs on the host from a
|
||||
timer with a key that can push to every repository in the GitHub organisation,
|
||||
which is why its unit installs a copy rather than linking into the checkout.
|
||||
|
||||
An ordinary edit is a diff you are about to read and is not reported; one git
|
||||
cannot see - hidden by `.gitignore`, `.git/info/exclude`, `assume-unchanged`
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# User unit for the GitHub mirror (#209), on a host that can push to GitHub.
|
||||
# Never in a dejima session: a sandbox may push only to its own forge.
|
||||
#
|
||||
# install -m 755 mirror/github-mirror.sh ~/.local/bin/dejima-github-mirror
|
||||
# cp mirror/dejima-github-mirror.service mirror/dejima-github-mirror.timer ~/.config/systemd/user/
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable --now dejima-github-mirror.timer
|
||||
#
|
||||
# A copy, not a link into the checkout: the timer holds a key that can push to
|
||||
# every repository in the organisation, and a session working in this
|
||||
# checkout can edit the script. Reinstall it after reviewing an update.
|
||||
#
|
||||
# ~/.config/dejima/github-mirror.env sets MIRROR_SOURCE, the forge
|
||||
# organisation each repository name is appended to, and whatever else
|
||||
# mirror/github-mirror.sh documents. The GitHub key belongs behind an ssh Host
|
||||
# alias named in MIRROR_TARGET, so it is not also offered to the forge. A
|
||||
# deploy key covers one repository, so this one is an account's key with push
|
||||
# access to every repository in the organisation:
|
||||
#
|
||||
# MIRROR_SOURCE=git@forge.example.com:dejima
|
||||
# MIRROR_TARGET=git@github-mirror:dejima-org
|
||||
#
|
||||
# A sync opens several connections per repository. One multiplexed connection
|
||||
# to GitHub's port 443 endpoint keeps a network that throttles bursts of
|
||||
# outbound ssh from timing out every repository after the first:
|
||||
#
|
||||
# Host github-mirror
|
||||
# HostName ssh.github.com
|
||||
# Port 443
|
||||
# User git
|
||||
# IdentityFile ~/.ssh/id_ed25519
|
||||
# IdentitiesOnly yes
|
||||
# ControlMaster auto
|
||||
# ControlPath ~/.ssh/cm-%C
|
||||
# ControlPersist 60
|
||||
#
|
||||
# ~/.config/dejima/github-mirror.scrub lists values replaced throughout the
|
||||
# published history, one "value replacement" pair per line. Adding one changes
|
||||
# history already published, so the next run refuses until it is given
|
||||
# --reset, once, by hand:
|
||||
#
|
||||
# gitea.internal.example gitea.example.com
|
||||
# 10.9.8. 192.0.2.
|
||||
[Unit]
|
||||
Description=Mirror the dejima repositories' main and release tags to GitHub
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=%h/.config/dejima/github-mirror.env
|
||||
ExecStart=%h/.local/bin/dejima-github-mirror
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Mirror the dejima repositories' main and release tags to GitHub, hourly
|
||||
|
||||
[Timer]
|
||||
OnCalendar=hourly
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Executable
+282
@@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publishes main and its release tags of every dejima repository to a
|
||||
# read-only mirror on GitHub (#209).
|
||||
#
|
||||
# MIRROR_SOURCE=<forge org URL> mirror/github-mirror.sh sync every repository
|
||||
# ... mirror/github-mirror.sh --dry-run build and print, push nothing
|
||||
# ... mirror/github-mirror.sh --reset replace a mirror built on other history
|
||||
# mirror/github-mirror.sh --settings configure the GitHub repos (needs gh)
|
||||
# mirror/github-mirror.sh --transform DIR rewrite a checkout in place
|
||||
#
|
||||
# Runs on the host, never in a session: a sandbox may push only to its own
|
||||
# forge, and this needs push access to GitHub.
|
||||
#
|
||||
# The GitHub copy is not byte-for-byte. Its history is the forge's main with
|
||||
# AI co-author trailers taken out of the commit messages, so no tool is
|
||||
# credited as a contributor, and the MIRROR_SCRUB values replaced in every
|
||||
# file and message (rewrite_history). That changes the SHA of every commit
|
||||
# from the first one touched on, but the same input always makes the same
|
||||
# commits. On top sits one extra commit, whose tree is the forge's with the
|
||||
# rewrites in transform() applied, and whose parents are the rewritten commit
|
||||
# and the previous mirror commit, so each sync is a fast-forward for anyone
|
||||
# tracking it. A tag on main is published as a lightweight tag on the
|
||||
# rewritten commit it maps to.
|
||||
#
|
||||
# A mirror whose main is not built on that history - an earlier rule, or a
|
||||
# forge main that was rewound - is refused. --reset force-pushes over it.
|
||||
#
|
||||
# Environment:
|
||||
# MIRROR_SOURCE forge URL each repository name is appended to, as
|
||||
# $MIRROR_SOURCE/<name> (required to sync)
|
||||
# MIRROR_ORG GitHub organisation, default dejima-org
|
||||
# MIRROR_TARGET push URL prefix, default git@github.com:$MIRROR_ORG
|
||||
# MIRROR_REPOS repositories, by their name on both sides
|
||||
# MIRROR_STATE working bare repositories, default ~/.cache/dejima-github-mirror
|
||||
# MIRROR_SCRUB values to replace throughout the published history, one
|
||||
# "value replacement" pair per line, default
|
||||
# ~/.config/dejima/github-mirror.scrub. Kept on the host
|
||||
# because the list itself is what it hides.
|
||||
set -euo pipefail
|
||||
|
||||
MIRROR_ORG="${MIRROR_ORG:-dejima-org}"
|
||||
MIRROR_TARGET="${MIRROR_TARGET:-git@github.com:$MIRROR_ORG}"
|
||||
MIRROR_REPOS="${MIRROR_REPOS:-dejima dejima-badge dejima-ci-hardener dejima-devbox-hardener dejima-guard .github}"
|
||||
MIRROR_STATE="${MIRROR_STATE:-${XDG_CACHE_HOME:-$HOME/.cache}/dejima-github-mirror}"
|
||||
MIRROR_SCRUB="${MIRROR_SCRUB:-${XDG_CONFIG_HOME:-$HOME/.config}/dejima/github-mirror.scrub}"
|
||||
export MIRROR_ORG MIRROR_SCRUB
|
||||
|
||||
NAME=""
|
||||
say() { echo "github-mirror: ${NAME:+$NAME: }$*"; }
|
||||
die() { say "$*" >&2; exit 1; }
|
||||
|
||||
# Rewrites a fast-export stream. Commit messages lose Co-Authored-By trailers
|
||||
# crediting Claude or anything else at anthropic.com, and every data block -
|
||||
# file contents and messages - has the MIRROR_SCRUB values replaced, matched
|
||||
# only where the value does not continue a longer name or number. Data
|
||||
# needing neither change passes through byte for byte, so its commit keeps
|
||||
# its SHA.
|
||||
rewrite_history() {
|
||||
perl -e '
|
||||
binmode STDIN; binmode STDOUT;
|
||||
my @pairs;
|
||||
if (open my $fh, "<", $ENV{MIRROR_SCRUB}) {
|
||||
while (<$fh>) {
|
||||
next if /^\s*(?:#|$)/;
|
||||
my ($from, $to) = split " ";
|
||||
defined $to or die "MIRROR_SCRUB: no replacement on line $.\n";
|
||||
push @pairs, [qr/(?<![\w.-])\Q$from\E/, $to];
|
||||
}
|
||||
}
|
||||
my $in_commit = 0;
|
||||
while (my $line = <STDIN>) {
|
||||
if ($line =~ /^commit /) {
|
||||
$in_commit = 1;
|
||||
} elsif ($line =~ /^data (\d+)$/) {
|
||||
my $len = $1;
|
||||
read(STDIN, my $data, $len) == $len or die "short data block\n";
|
||||
if ($in_commit
|
||||
&& $data =~ s/^Co-Authored-By:[^\n]*(?:claude|anthropic\.com)[^\n]*(?:\n|\z)//mgi) {
|
||||
$data =~ s/\s+\z/\n/;
|
||||
}
|
||||
$data =~ s/$_->[0]/$_->[1]/g for @pairs;
|
||||
$in_commit = 0;
|
||||
print "data ", length($data), "\n", $data;
|
||||
next;
|
||||
}
|
||||
print $line;
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
# Fails if a MIRROR_SCRUB value survives in a tree about to be published.
|
||||
check_scrubbed() {
|
||||
[ -f "$MIRROR_SCRUB" ] || return 0
|
||||
local from to
|
||||
while read -r from to; do
|
||||
case "$from" in ''|'#'*) continue ;; esac
|
||||
if git -C "$1" grep -qIF -- "$from"; then
|
||||
die "a MIRROR_SCRUB value is still in the tree to publish"
|
||||
fi
|
||||
done < "$MIRROR_SCRUB"
|
||||
}
|
||||
|
||||
# The rewritten commit a forge commit became, from the two runs' marks.
|
||||
rewritten() {
|
||||
local mark
|
||||
mark="$(awk -v c="$1" '$2 == c { print $1; exit }' "$STATE/source.marks")"
|
||||
[ -n "$mark" ] || die "no rewritten commit for $1"
|
||||
awk -v m="$mark" '$1 == m { print $2; exit }' "$STATE/clean.marks"
|
||||
}
|
||||
|
||||
WORK=""
|
||||
STATE=""
|
||||
cleanup() {
|
||||
[ -n "$WORK" ] || return 0
|
||||
git -C "$STATE" worktree remove --force "$WORK" 2>/dev/null || true
|
||||
rm -rf "$WORK"
|
||||
}
|
||||
|
||||
# Rewrites a checkout for GitHub. Each rewrite is checked, so a README that
|
||||
# drifted away from what this expects stops the mirror instead of publishing
|
||||
# the forge's version. Running it twice changes nothing, so a clone of the
|
||||
# mirror still passes test/mirror.bats.
|
||||
transform() {
|
||||
local dir="$1"
|
||||
local readme="$dir/README.md"
|
||||
# perl -i would write a symlink's target back as the README, publishing it.
|
||||
[ -f "$readme" ] && [ ! -L "$readme" ] || die "README.md is missing or a symlink in $dir"
|
||||
|
||||
# Repository links and clone URLs move to the org on GitHub, in code as
|
||||
# well as docs; issue and pull request links stay on the forge, which is
|
||||
# the only place those exist. Gitea's /src/branch/ is GitHub's /blob/, and
|
||||
# its /raw/branch/ GitHub's /raw/.
|
||||
# Exit 1 from git grep is no match, which a repository may well have. The
|
||||
# paths come from the forge, so -- keeps one from being read as a perl option.
|
||||
(cd "$dir" && { git grep -lzIF 'gitbox.branderson.io/dejima/' || [ $? -eq 1 ]; } | xargs -0r perl -pi -e '
|
||||
s{https://gitbox\.branderson\.io/dejima/([\w.-]++)(?!/(?:issues|pulls)/)}{https://github.com/$ENV{MIRROR_ORG}/$1}g;
|
||||
s{(https://github\.com/\Q$ENV{MIRROR_ORG}\E/[\w.-]+)/src/branch/}{$1/blob/}g;
|
||||
s{(https://github\.com/\Q$ENV{MIRROR_ORG}\E/[\w.-]+)/raw/branch/}{$1/raw/}g;
|
||||
' --)
|
||||
|
||||
# GitHub follows the reader's light/dark setting through <picture>, which
|
||||
# the forge cannot; see the comment above the tag in README.md.
|
||||
if [ -f "$dir/assets/dejima-wordmark-lightmode.png" ]; then
|
||||
perl -0pi -e '
|
||||
s{<img src="assets/dejima-wordmark-darkmode\.png" alt="dejima" height="72">}
|
||||
{<picture><source media="(prefers-color-scheme: dark)" srcset="assets/dejima-wordmark-darkmode.png"><img src="assets/dejima-wordmark-lightmode.png" alt="dejima" height="72"></picture>}
|
||||
' "$readme"
|
||||
grep -qF 'darkmode.png"><img src="assets/dejima-wordmark-lightmode.png"' "$readme" \
|
||||
|| die "README.md: the wordmark tag was not found"
|
||||
fi
|
||||
|
||||
# The first thing a visitor to a mirror does is open a pull request. Under
|
||||
# the rule after the wordmark, or under the title where there is none.
|
||||
grep -q '^> \*\*This is a read-only mirror' "$readme" || perl -0pi -e '
|
||||
s{^(---|# [^\n]*)\n}{$1\n\n> **This is a read-only mirror.** dejima is developed on a private forge,\n> and issues and pull requests are not accepted here. See\n> [CONTRIBUTING.md](https://github.com/$ENV{MIRROR_ORG}/dejima/blob/main/CONTRIBUTING.md).\n}m
|
||||
' "$readme"
|
||||
grep -q '^> \*\*This is a read-only mirror' "$readme" \
|
||||
|| die "README.md: no --- line or title to put the mirror notice under"
|
||||
}
|
||||
|
||||
# Issues, pull requests and Actions off. Settings rather than content, so a
|
||||
# sync does not reapply them; run once after creating the repositories.
|
||||
settings() {
|
||||
command -v gh >/dev/null || die "--settings needs the gh CLI"
|
||||
local repo
|
||||
for repo in $MIRROR_REPOS; do
|
||||
gh api -X PATCH "repos/$MIRROR_ORG/$repo" --silent \
|
||||
-F has_issues=false -F has_pull_requests=false -F has_projects=false \
|
||||
-F has_wiki=false -F has_discussions=false
|
||||
gh api -X PUT "repos/$MIRROR_ORG/$repo/actions/permissions" --silent -F enabled=false
|
||||
done
|
||||
}
|
||||
|
||||
sync_repo() {
|
||||
NAME="$1"
|
||||
STATE="$MIRROR_STATE/$NAME.git"
|
||||
local source="$MIRROR_SOURCE/$NAME" target="$MIRROR_TARGET/$NAME"
|
||||
local git=(git -C "$STATE")
|
||||
trap cleanup EXIT
|
||||
[ -d "$STATE" ] || git init -q --bare "$STATE"
|
||||
|
||||
"${git[@]}" fetch -q --no-tags --prune "$source" \
|
||||
'+refs/heads/main:refs/source/main' '+refs/tags/*:refs/source/tags/*'
|
||||
local src clean prev=""
|
||||
src="$("${git[@]}" rev-parse refs/source/main)"
|
||||
|
||||
# Rebuilt from the whole history each run.
|
||||
"${git[@]}" fast-export --signed-tags=strip --reencode=yes \
|
||||
--export-marks="$STATE/source.marks" \
|
||||
--refspec refs/source/main:refs/clean/main refs/source/main \
|
||||
| rewrite_history \
|
||||
| "${git[@]}" fast-import --quiet --force --export-marks="$STATE/clean.marks"
|
||||
clean="$("${git[@]}" rev-parse refs/clean/main)"
|
||||
|
||||
# An absent main (exit 2) is a first run; anything else is a real failure.
|
||||
local rc=0
|
||||
git ls-remote -q --exit-code "$target" refs/heads/main >/dev/null || rc=$?
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
"${git[@]}" fetch -q --no-tags "$target" '+refs/heads/main:refs/mirror/main'
|
||||
prev="$("${git[@]}" rev-parse refs/mirror/main)"
|
||||
elif [ "$rc" -ne 2 ]; then
|
||||
die "cannot read $target"
|
||||
fi
|
||||
local force=""
|
||||
if [ -n "$prev" ] && ! "${git[@]}" merge-base --is-ancestor "$prev^1" "$clean"; then
|
||||
[ -n "$RESET" ] \
|
||||
|| die "GitHub's main is not built on the history this run publishes; --reset force-pushes over it"
|
||||
prev=""
|
||||
force=+
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
"${git[@]}" worktree add -q --detach "$WORK" "$clean"
|
||||
transform "$WORK"
|
||||
check_scrubbed "$WORK"
|
||||
git -C "$WORK" add -A
|
||||
local tree
|
||||
tree="$(git -C "$WORK" write-tree)"
|
||||
|
||||
if [ -n "$prev" ] && [ "$("${git[@]}" rev-parse "$prev^1")" = "$clean" ] \
|
||||
&& [ "$("${git[@]}" rev-parse "$prev^{tree}")" = "$tree" ]; then
|
||||
say "already at ${src:0:12}"
|
||||
else
|
||||
# The forge commit's committer and date, so the same input always
|
||||
# makes the same commit.
|
||||
local name email date commit
|
||||
IFS=$'\t' read -r name email date < <("${git[@]}" log -1 --format='%cn%x09%ce%x09%cI' "$src")
|
||||
commit="$(GIT_AUTHOR_NAME="$name" GIT_AUTHOR_EMAIL="$email" GIT_AUTHOR_DATE="$date" \
|
||||
GIT_COMMITTER_NAME="$name" GIT_COMMITTER_EMAIL="$email" GIT_COMMITTER_DATE="$date" \
|
||||
"${git[@]}" commit-tree "$tree" -p "$clean" ${prev:+-p "$prev"} \
|
||||
-m "Mirror ${src:0:12} for GitHub" \
|
||||
-m "Forge links, the README header and commit trailers rewritten by dejima's mirror/github-mirror.sh.")"
|
||||
"${git[@]}" update-ref refs/mirror/next "$commit"
|
||||
[ -n "$DRY_RUN" ] || "${git[@]}" push -q "$target" "$force$commit:refs/heads/main"
|
||||
say "main is ${src:0:12} as ${commit:0:12}"
|
||||
fi
|
||||
|
||||
# Only tags on main: one on another branch would publish unreleased work.
|
||||
local ref
|
||||
local refspecs=()
|
||||
while IFS= read -r ref; do
|
||||
if "${git[@]}" merge-base --is-ancestor "$ref" "$src"; then
|
||||
local at
|
||||
at="$(rewritten "$("${git[@]}" rev-parse "$ref^{commit}")")"
|
||||
refspecs+=("$force$at:refs/tags/${ref#refs/source/tags/}")
|
||||
say "tag ${ref#refs/source/tags/} at ${at:0:12}"
|
||||
fi
|
||||
done < <("${git[@]}" for-each-ref --format='%(refname)' refs/source/tags/)
|
||||
[ "${#refspecs[@]}" -eq 0 ] || [ -n "$DRY_RUN" ] \
|
||||
|| "${git[@]}" push -q "$target" "${refspecs[@]}"
|
||||
}
|
||||
|
||||
# Each repository in a subshell of its own, so one that fails does not keep
|
||||
# the rest from syncing. Not `( ... ) || ...`, which turns off set -e inside.
|
||||
sync() {
|
||||
[ -n "${MIRROR_SOURCE:-}" ] || die "MIRROR_SOURCE is not set"
|
||||
local repo rc failed=""
|
||||
for repo in $MIRROR_REPOS; do
|
||||
set +e
|
||||
(set -e; sync_repo "$repo")
|
||||
rc=$?
|
||||
set -e
|
||||
[ "$rc" -eq 0 ] || failed="$failed $repo"
|
||||
done
|
||||
[ -z "$failed" ] || die "failed:$failed"
|
||||
}
|
||||
|
||||
DRY_RUN=""
|
||||
RESET=""
|
||||
case "${1:-}" in
|
||||
--settings) settings; exit ;;
|
||||
--transform) [ -n "${2:-}" ] || die "--transform needs a directory"; transform "$2"; exit ;;
|
||||
esac
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--reset) RESET=1 ;;
|
||||
*) sed -n '5,9p' "$0" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
sync
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env bats
|
||||
# mirror/github-mirror.sh: what the GitHub copies are rewritten to, and how
|
||||
# their main is built. Nothing here pushes; --dry-run stops before the push, which
|
||||
# is the one line these tests leave to the operator's first sync.
|
||||
|
||||
setup() {
|
||||
REPO_ROOT="$BATS_TEST_DIRNAME/.."
|
||||
MIRROR="$REPO_ROOT/mirror/github-mirror.sh"
|
||||
T="$(mktemp -d)"
|
||||
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
|
||||
export GIT_AUTHOR_NAME=test GIT_AUTHOR_EMAIL=test@example.com \
|
||||
GIT_COMMITTER_NAME=test GIT_COMMITTER_EMAIL=test@example.com
|
||||
|
||||
# The forge: this tree's docs, as one commit on main.
|
||||
F="$T/forge/dejima"
|
||||
git init -q -b main "$F"
|
||||
cp -r "$REPO_ROOT"/*.md "$REPO_ROOT/docs" "$REPO_ROOT/assets" "$F/"
|
||||
git -C "$F" add -A
|
||||
git -C "$F" commit -qm base
|
||||
mkdir "$T/github"
|
||||
git init -q --bare "$T/github/dejima"
|
||||
export MIRROR_SOURCE="$T/forge" MIRROR_TARGET="$T/github" MIRROR_STATE="$T/state" \
|
||||
MIRROR_REPOS=dejima MIRROR_SCRUB="$T/scrub"
|
||||
unset MIRROR_ORG
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$T"
|
||||
}
|
||||
|
||||
mirrored() {
|
||||
git -C "$MIRROR_STATE/dejima.git" rev-parse refs/mirror/next
|
||||
}
|
||||
|
||||
@test "the mirror's README follows the reader's colour scheme" {
|
||||
"$MIRROR" --transform "$F"
|
||||
local readme="$F/README.md"
|
||||
grep -qF '<source media="(prefers-color-scheme: dark)" srcset="assets/dejima-wordmark-darkmode.png">' "$readme"
|
||||
grep -qF '<img src="assets/dejima-wordmark-lightmode.png" alt="dejima" height="72"></picture>' "$readme"
|
||||
}
|
||||
|
||||
@test "the mirror's README says it takes no issues or pull requests, above the fold" {
|
||||
"$MIRROR" --transform "$F"
|
||||
local notice heading
|
||||
notice="$(grep -n 'This is a read-only mirror' "$F/README.md" | cut -d: -f1)"
|
||||
heading="$(grep -n '^## ' "$F/README.md" | head -1 | cut -d: -f1)"
|
||||
[ -n "$notice" ] && [ "$notice" -lt "$heading" ]
|
||||
grep -q 'issues and pull requests are not accepted here' "$F/README.md"
|
||||
}
|
||||
|
||||
@test "repository links move to GitHub and issue links stay on the forge" {
|
||||
grep -q 'gitbox.branderson.io/dejima/dejima/\(issues\|pulls\)/' "$F/CHANGELOG.md"
|
||||
"$MIRROR" --transform "$F"
|
||||
cd "$F"
|
||||
run grep -rhoE 'https://gitbox\.branderson\.io/dejima/[^)]*' --include='*.md' .
|
||||
local link
|
||||
for link in "${lines[@]}"; do
|
||||
[[ "$link" =~ ^https://gitbox\.branderson\.io/dejima/dejima/(issues|pulls)/[0-9]+$ ]] \
|
||||
|| { echo "still on the forge: $link"; return 1; }
|
||||
done
|
||||
grep -qF '](https://github.com/dejima-org/dejima)' README.md
|
||||
grep -qF '(https://github.com/dejima-org/dejima-guard/blob/main/SECURITY.md)' docs/threat-model.md
|
||||
! grep -rq 'github.com/dejima-org/[^)]*/src/branch/' --include='*.md' .
|
||||
}
|
||||
|
||||
@test "transforming twice changes nothing, so a clone of the mirror passes this suite" {
|
||||
"$MIRROR" --transform "$F"
|
||||
cp -r "$F" "$T/once"
|
||||
"$MIRROR" --transform "$F"
|
||||
diff -r -x .git "$T/once" "$F"
|
||||
}
|
||||
|
||||
@test "a README the transform no longer recognises stops the mirror" {
|
||||
sed -i 's/dejima-wordmark-darkmode.png/renamed.png/' "$F/README.md"
|
||||
run "$MIRROR" --transform "$F"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"wordmark tag was not found"* ]]
|
||||
}
|
||||
|
||||
@test "the mirror commit sits on the forge's main, and the same input makes the same commit" {
|
||||
run "$MIRROR" --dry-run
|
||||
[ "$status" -eq 0 ]
|
||||
local first src
|
||||
first="$(mirrored)"
|
||||
src="$(git -C "$F" rev-parse main)"
|
||||
[ "$(git -C "$MIRROR_STATE/dejima.git" rev-parse "$first^1")" = "$src" ]
|
||||
run git -C "$MIRROR_STATE/dejima.git" rev-parse -q --verify "$first^2"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
MIRROR_STATE="$T/state2" run "$MIRROR" --dry-run
|
||||
[ "$(git -C "$T/state2/dejima.git" rev-parse refs/mirror/next)" = "$first" ]
|
||||
}
|
||||
|
||||
@test "each sync fast-forwards the previous mirror commit" {
|
||||
"$MIRROR" --dry-run
|
||||
local prev
|
||||
prev="$(mirrored)"
|
||||
git -C "$MIRROR_TARGET/dejima" fetch -q "$MIRROR_STATE/dejima.git" "$prev:refs/heads/main"
|
||||
|
||||
echo change >> "$F/docs/ci.md"
|
||||
git -C "$F" commit -qam change
|
||||
"$MIRROR" --dry-run
|
||||
local next
|
||||
next="$(mirrored)"
|
||||
[ "$(git -C "$MIRROR_STATE/dejima.git" rev-parse "$next^1")" = "$(git -C "$F" rev-parse main)" ]
|
||||
[ "$(git -C "$MIRROR_STATE/dejima.git" rev-parse "$next^2")" = "$prev" ]
|
||||
|
||||
git -C "$MIRROR_TARGET/dejima" fetch -q "$MIRROR_STATE/dejima.git" "+$next:refs/heads/main"
|
||||
run "$MIRROR" --dry-run
|
||||
[[ "$output" == *"already at"* ]]
|
||||
}
|
||||
|
||||
@test "only tags on main are mirrored" {
|
||||
git -C "$F" tag released
|
||||
git -C "$F" checkout -qb development
|
||||
git -C "$F" commit -q --allow-empty -m unreleased
|
||||
git -C "$F" tag unreleased
|
||||
run "$MIRROR" --dry-run
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"tag released"* ]]
|
||||
[[ "$output" != *"tag unreleased"* ]]
|
||||
}
|
||||
|
||||
@test "clone URLs in code move too, and a repo with no wordmark gets the notice under its title" {
|
||||
# Assembled, so the mirror's copy of this file is not itself rewritten.
|
||||
local B="$T/forge/dejima-badge" forge="https://gitbox.branderson.io"
|
||||
git init -q -b main "$B"
|
||||
printf '# Developed in dejima\n\nSee [dejima](%s/dejima/dejima).\n\n## Use\n\n curl -fLO %s/dejima/dejima-badge/raw/branch/main/dejima-badge.svg\n' "$forge" "$forge" > "$B/README.md"
|
||||
printf '#!/bin/sh\nURL=${URL:-%s/dejima/dejima.git}\n' "$forge" > "$B/clone"
|
||||
git -C "$B" add -A
|
||||
"$MIRROR" --transform "$B"
|
||||
[ "$(sed -n 3p "$B/README.md")" = '> **This is a read-only mirror.** dejima is developed on a private forge,' ]
|
||||
grep -qF 'https://github.com/dejima-org/dejima-badge/raw/main/dejima-badge.svg' "$B/README.md"
|
||||
grep -qF 'URL=${URL:-https://github.com/dejima-org/dejima.git}' "$B/clone"
|
||||
}
|
||||
|
||||
@test "a repository that fails does not stop the others, and the run says which" {
|
||||
MIRROR_REPOS="missing dejima" run "$MIRROR" --dry-run
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"dejima: main is "* ]]
|
||||
[[ "$output" == *"failed: missing"* ]]
|
||||
}
|
||||
|
||||
@test "a repository with no forge links still gets its notice" {
|
||||
local B="$T/forge/plain"
|
||||
git init -q -b main "$B"
|
||||
printf '# Plain\n\nNothing to rewrite.\n' > "$B/README.md"
|
||||
git -C "$B" add -A
|
||||
"$MIRROR" --transform "$B"
|
||||
grep -q '^> \*\*This is a read-only mirror' "$B/README.md"
|
||||
}
|
||||
|
||||
@test "a file named like a perl option is rewritten, not run" {
|
||||
local forge="https://gitbox.branderson.io"
|
||||
printf 'See %s/dejima/dejima-guard\n' "$forge" > "$F/-eBEGIN{exit 3}"
|
||||
git -C "$F" add -A
|
||||
"$MIRROR" --transform "$F"
|
||||
grep -qF 'https://github.com/dejima-org/dejima-guard' "$F/-eBEGIN{exit 3}"
|
||||
}
|
||||
|
||||
@test "a README that is a symlink stops the mirror instead of publishing its target" {
|
||||
local B="$T/forge/linked"
|
||||
git init -q -b main "$B"
|
||||
printf '# Not for publishing\n' > "$T/outside"
|
||||
ln -s "$T/outside" "$B/README.md"
|
||||
git -C "$B" add -A
|
||||
run "$MIRROR" --transform "$B"
|
||||
[ "$status" -ne 0 ]
|
||||
[ -L "$B/README.md" ]
|
||||
[ "$(cat "$T/outside")" = '# Not for publishing' ]
|
||||
}
|
||||
|
||||
published() {
|
||||
git -C "$MIRROR_STATE/dejima.git" "$@" refs/mirror/next
|
||||
}
|
||||
|
||||
@test "no AI co-author trailer reaches the published history, and other commits keep their SHA" {
|
||||
local base
|
||||
base="$(git -C "$F" rev-parse HEAD)"
|
||||
echo change >> "$F/docs/ci.md"
|
||||
git -C "$F" commit -qa -m change -m 'Keeps this line.' \
|
||||
-m 'Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>'
|
||||
"$MIRROR" --dry-run
|
||||
run published log --format=%B
|
||||
[[ "$output" != *"Co-Authored-By"* ]] || { echo "$output"; return 1; }
|
||||
[[ "$output" == *"Keeps this line."* ]]
|
||||
run published rev-list --max-parents=0
|
||||
[ "$output" = "$base" ]
|
||||
}
|
||||
|
||||
@test "MIRROR_SCRUB values are replaced in every file and message in the history" {
|
||||
printf '# example\ngitbox.internal.example gitbox.example.com\n10.9.8. 192.0.2.\n' > "$T/scrub"
|
||||
printf 'forge = gitbox.internal.example\nhost = 10.9.8.7\nother = 110.9.8.7\n' > "$F/docs/net.md"
|
||||
git -C "$F" add -A
|
||||
git -C "$F" commit -qm 'Point at gitbox.internal.example'
|
||||
git -C "$F" rm -q docs/net.md
|
||||
git -C "$F" commit -qm 'Take it back out'
|
||||
"$MIRROR" --dry-run
|
||||
run published log -p --format=%B
|
||||
[[ "$output" != *"internal.example"* ]] || { echo "$output"; return 1; }
|
||||
[[ "$output" == *"Point at gitbox.example.com"* ]]
|
||||
[[ "$output" == *"host = 192.0.2.7"* ]]
|
||||
[[ "$output" == *"other = 110.9.8.7"* ]]
|
||||
}
|
||||
|
||||
@test "a scrub value the rewrite could not reach stops the mirror" {
|
||||
printf 'gitbox.internal.example gitbox.example.com\n' > "$T/scrub"
|
||||
echo 'see xgitbox.internal.example' > "$F/docs/net.md"
|
||||
git -C "$F" add -A
|
||||
git -C "$F" commit -qm net
|
||||
run "$MIRROR" --dry-run
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"MIRROR_SCRUB value is still in the tree"* ]]
|
||||
}
|
||||
|
||||
@test "tags are published on the rewritten commit they map to" {
|
||||
git -C "$F" commit -q --allow-empty -m tagged \
|
||||
-m 'Co-Authored-By: Claude <noreply@anthropic.com>'
|
||||
git -C "$F" tag release
|
||||
run "$MIRROR" --dry-run
|
||||
[ "$status" -eq 0 ]
|
||||
local at
|
||||
at="$(git -C "$MIRROR_STATE/dejima.git" rev-parse refs/clean/main)"
|
||||
[ "$at" != "$(git -C "$F" rev-parse release)" ]
|
||||
[[ "$output" == *"tag release at ${at:0:12}"* ]]
|
||||
}
|
||||
|
||||
@test "a mirror built on other history is refused, and --reset replaces it" {
|
||||
# What GitHub held before trailers were rewritten: a mirror commit whose
|
||||
# first parent is the forge commit itself.
|
||||
git -C "$F" commit -q --allow-empty -m tagged \
|
||||
-m 'Co-Authored-By: Claude <noreply@anthropic.com>'
|
||||
local old
|
||||
old="$(git -C "$F" commit-tree "$(git -C "$F" rev-parse HEAD^{tree})" -p HEAD -m mirror)"
|
||||
git -C "$T/github/dejima" fetch -q "$F" "$old:refs/heads/main"
|
||||
|
||||
run "$MIRROR" --dry-run
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"--reset force-pushes over it"* ]]
|
||||
|
||||
run "$MIRROR" --dry-run --reset
|
||||
[ "$status" -eq 0 ]
|
||||
run git -C "$MIRROR_STATE/dejima.git" rev-parse -q --verify refs/mirror/next^2
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
@@ -485,6 +485,45 @@ make_cert() {
|
||||
return 0
|
||||
}
|
||||
|
||||
@test "an oversized body is refused before the 100 Continue that invites it" {
|
||||
# The test above is about what the client ends up with, and it can pass
|
||||
# either way when the timing is kind: the proxy used to answer
|
||||
# `100 Continue` first and refuse from the body reader afterwards, so
|
||||
# whether a client read the 413 or only the 100 was a race. This one asks
|
||||
# the question the race cannot reach - what the FIRST answer is - and never
|
||||
# sends a body at all.
|
||||
start_proxy
|
||||
|
||||
run python3 - "$SOCK" "$PLACEHOLDER" <<'PROBE'
|
||||
import socket, sys
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
sock.connect(sys.argv[1])
|
||||
# Declared far over the cap, and not one byte of it sent: a client offering a
|
||||
# body is entitled to be told no before it starts.
|
||||
sock.sendall(
|
||||
b"POST /v1/messages HTTP/1.1\r\n"
|
||||
b"Host: x\r\n"
|
||||
b"Authorization: Bearer " + sys.argv[2].encode() + b"\r\n"
|
||||
b"Content-Type: application/json\r\n"
|
||||
b"Content-Length: 99999999999\r\n"
|
||||
b"Expect: 100-continue\r\n\r\n")
|
||||
data = b""
|
||||
while b"\r\n" not in data:
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
print(data.split(b"\r\n")[0].decode("ascii", "replace"))
|
||||
PROBE
|
||||
[ "$status" -eq 0 ] || { echo "the probe failed: $output"; return 1; }
|
||||
[[ "$output" == *"413"* ]] \
|
||||
|| { echo "the first answer was not a refusal: $output"; return 1; }
|
||||
grep -q 'refused a request body' "$TD/stderr" \
|
||||
|| { echo "nothing logged: $(cat "$TD/stderr")"; return 1; }
|
||||
}
|
||||
|
||||
@test "a normal-sized request body still passes through" {
|
||||
# The cap must be far above anything real. A conversation with attachments
|
||||
# is the biggest thing a Messages request carries.
|
||||
|
||||
@@ -78,6 +78,7 @@ suites_for() {
|
||||
forges/*|workflows/*|harnesses/*|tools/*) \
|
||||
echo 'providers static audit config conversation' ;;
|
||||
install.sh) echo 'config audit providers remote static' ;;
|
||||
mirror/*) echo 'mirror static' ;;
|
||||
|
||||
# static.bats asserts on files it does not own, so a shipped doc or a
|
||||
# docker/ file needs it as well as whatever else the change touched -
|
||||
@@ -105,13 +106,14 @@ suites_for() {
|
||||
# would otherwise answer for it. The docs themselves are asserted
|
||||
# against by more than static.bats - the README's axis lists, the
|
||||
# provider tables, the nginx example and the remote-access page each
|
||||
# have a suite checking the code agrees with them.
|
||||
# have a suite checking the code agrees with them, and mirror.bats runs
|
||||
# the GitHub rewrite over them.
|
||||
#
|
||||
# So a doc-only edit runs more than static alone. That is the cost of
|
||||
# the lane answering for what reads a file, and it is the safe
|
||||
# direction: narrowing this back would report a doc change as green
|
||||
# from suites that assert against it.
|
||||
docs/*|README.md|*.md) echo 'static config providers remote' ;;
|
||||
docs/*|README.md|*.md) echo 'static config providers remote mirror' ;;
|
||||
|
||||
*) echo ALL ;;
|
||||
esac
|
||||
|
||||
@@ -30,6 +30,7 @@ list_scripts() {
|
||||
# these the last scripts that should go unchecked, not the first -
|
||||
# and leaving them out hid a real fault in the release gate itself.
|
||||
ls test/*.sh 2>/dev/null || true
|
||||
ls mirror/*.sh 2>/dev/null || true
|
||||
} | sort -u | while read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
# Sourced fragments are not executed, so they carry no shebang and the
|
||||
|
||||
+22
-3
@@ -7825,8 +7825,9 @@ EOF
|
||||
grep -qF "$piece" <<< "$rows" \
|
||||
|| { echo "the README stack table has no row for $piece"; return 1; }
|
||||
done
|
||||
# Either host: mirror/github-mirror.sh rewrites these for the GitHub copy.
|
||||
for piece in dejima-devbox-hardener dejima-guard dejima-ci-hardener; do
|
||||
grep -qF "https://gitbox.branderson.io/dejima/$piece" <<< "$rows" \
|
||||
grep -qE "https://(gitbox\.branderson\.io/dejima|github\.com/dejima-org)/$piece" <<< "$rows" \
|
||||
|| { echo "$piece is named in the README but not linked"; return 1; }
|
||||
done
|
||||
|
||||
@@ -7898,9 +7899,9 @@ EOF
|
||||
# Linked, like the other two, and pointed at its own SECURITY.md for the
|
||||
# comparisons this doc deliberately does not lead with.
|
||||
local tm="$REPO_ROOT/docs/threat-model.md"
|
||||
grep -q '(https://gitbox.branderson.io/dejima/dejima-guard)' "$tm" \
|
||||
grep -qE '\(https://(gitbox\.branderson\.io/dejima|github\.com/dejima-org)/dejima-guard\)' "$tm" \
|
||||
|| { echo "dejima-guard is named but not linked"; return 1; }
|
||||
grep -q 'dejima-guard/src/branch/main/SECURITY.md' "$tm" \
|
||||
grep -qE 'dejima-guard/(src/branch|blob)/main/SECURITY.md' "$tm" \
|
||||
|| { echo "nothing points at the guard's own security policy"; return 1; }
|
||||
}
|
||||
|
||||
@@ -9551,6 +9552,24 @@ _provider_proxied_domain() {
|
||||
done
|
||||
}
|
||||
|
||||
@test "the GitHub mirror is in the audit's TCB list, and is installed as a copy" {
|
||||
# The timer runs it on the host with a key that can push to every
|
||||
# repository in the organisation. A symlink into this checkout would run a
|
||||
# session's edit within the hour, before any exit audit had seen it.
|
||||
local dirs
|
||||
dirs="$(sed -n '/^tcb_snapshot()/,/^}/p' "$REPO_ROOT/bin/dejima-audit" \
|
||||
| sed -n 's/.*for dir in \(.*\); do/\1/p')"
|
||||
case " $dirs " in
|
||||
*" mirror "*) ;;
|
||||
*) echo "mirror is not in the TCB list: $dirs"; return 1 ;;
|
||||
esac
|
||||
local unit="$REPO_ROOT/mirror/dejima-github-mirror.service"
|
||||
grep -q '^# install -m 755 mirror/github-mirror.sh ~/.local/bin/dejima-github-mirror$' "$unit" \
|
||||
|| { echo "the unit no longer installs the script as a copy"; return 1; }
|
||||
run grep -n 'ln -s' "$unit"
|
||||
[ "$status" -eq 1 ] || { echo "the unit links the script into the checkout: $output"; return 1; }
|
||||
}
|
||||
|
||||
@test "the transcript format is the harness's, the store is dejima's" {
|
||||
# What a compaction looks like in a transcript is a fact about the
|
||||
# harness; where the summary is kept, that nothing mounts it, and what
|
||||
|
||||
Reference in New Issue
Block a user