Push auto-release tags as renovate-bot so release.yml fires, and give the Docker Hub page its short description #196
@@ -81,7 +81,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v7.0.1
|
- uses: actions/checkout@v7.0.1
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
# Publish the named release's own tree, so the page never pairs one Plainpages tag with another
|
# Publish the named release's own tree, so the page never pairs one Plainpages tag with another
|
||||||
# release's sidecar pins. A version that was never released fails here.
|
# release's sidecar pins, which is also why it never syncs on a push to main. A version that
|
||||||
|
# was never released fails here.
|
||||||
- uses: actions/checkout@v7.0.1
|
- uses: actions/checkout@v7.0.1
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ jobs:
|
|||||||
- uses: actions/checkout@v7.0.1
|
- uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
# A persisted token rides every request as an extraheader, outranking the PAT in the push
|
||||||
|
# URL: Gitea then sees the Actions bot push the tag and starts no workflow for it.
|
||||||
|
persist-credentials: false
|
||||||
- name: Tag a release for what Renovate merged
|
- name: Tag a release for what Renovate merged
|
||||||
env:
|
env:
|
||||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
|
|||||||
@@ -340,10 +340,11 @@ Revisit only if the stated reason stops holding.
|
|||||||
*text* guard — the test image ships neither `git` nor `bash`. This is why the Docker Hub overview is
|
*text* guard — the test image ships neither `git` nor `bash`. This is why the Docker Hub overview is
|
||||||
`release-tooling/dockerhub-overview.md.tmpl` and not a `.md`: a release reads it and a unit test
|
`release-tooling/dockerhub-overview.md.tmpl` and not a `.md`: a release reads it and a unit test
|
||||||
guards it, so giving it a `.md` name would let a broken `{{VERSION}}` merge with its own guard
|
guards it, so giving it a `.md` name would let a broken `{{VERSION}}` merge with its own guard
|
||||||
skipped. `README.md` is the one markdown a test reads — `release-tooling/contract-version.test.ts`
|
skipped. `README.md` is the one markdown tests read — `release-tooling/contract-version.test.ts`
|
||||||
checks its `apiVersion` samples — and a README-only change skips that check; accepted, because
|
checks its `apiVersion` samples, `release-tooling/dockerhub-overview.test.ts` that the Hub page
|
||||||
those samples are illustrative and the copies that matter (`examples/`, `views/`, the template) are
|
says only what it says — and a README-only change skips both; accepted, because those samples are
|
||||||
gated. **Valid while no markdown file is rendered or executed.**
|
illustrative, the copies that matter (`examples/`, `views/`, the template) are gated, and the next
|
||||||
|
gated change catches a README that dropped a Hub command. **Valid while no markdown file is rendered or executed.**
|
||||||
- **CI docker logins share the runner host's Docker config.** The act_runner is host-mode, so
|
- **CI docker logins share the runner host's Docker config.** The act_runner is host-mode, so
|
||||||
`docker login`/`logout` in the workflows mutate one shared `~/.docker/config.json`: concurrent jobs
|
`docker login`/`logout` in the workflows mutate one shared `~/.docker/config.json`: concurrent jobs
|
||||||
can race (one job's logout can 401 another's push — recover by re-running), and tokens sit in that
|
can race (one job's logout can 401 another's push — recover by re-running), and tokens sit in that
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ at boot. Entries start at 0.3.0.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Every release reaches Docker Hub, and the Hub page has a short description
|
||||||
|
|
||||||
|
Releases 0.4.1 to 0.4.20 were tagged in git but never published to Docker Hub or as semver tags in
|
||||||
|
the Gitea registry; they are now, and every later release is. The Hub page gains a one-line
|
||||||
|
description.
|
||||||
|
|
||||||
### The admin example stops an administrator locking themselves out through a group
|
### The admin example stops an administrator locking themselves out through a group
|
||||||
|
|
||||||
On the Groups screen, unticking a permission on a group you are in, removing yourself (or a nested
|
On the Groups screen, unticking a permission on a group you are in, removing yourself (or a nested
|
||||||
|
|||||||
@@ -1591,10 +1591,11 @@ tags sync to [Docker Hub](https://hub.docker.com/r/larvit/plainpages), releases
|
|||||||
The [contract check](#contract-versioning) guards the tag before anything is published, refusing one
|
The [contract check](#contract-versioning) guards the tag before anything is published, refusing one
|
||||||
whose `major.minor` disagrees with `HOST_API_VERSION` and naming the value to set.
|
whose `major.minor` disagrees with `HOST_API_VERSION` and naming the value to set.
|
||||||
|
|
||||||
**The Docker Hub overview** is published by a separate `publish-overview` job from
|
**The Docker Hub overview** and short description are published by a separate `publish-overview`
|
||||||
[`release-tooling/dockerhub-overview.md.tmpl`](release-tooling/dockerhub-overview.md.tmpl), with
|
job from [`release-tooling/dockerhub-overview.md.tmpl`](release-tooling/dockerhub-overview.md.tmpl), with
|
||||||
`{{VERSION}}` rendered to the release, so the Plainpages tag it tells adopters to pull cannot go
|
`{{VERSION}}` rendered to the release, so the Plainpages tag it tells adopters to pull cannot go
|
||||||
stale. Its sidecar pins are Renovate-managed and gated against this repo's own compose files, so the
|
stale. The description must open a sentence of this README, and every command the overview shows
|
||||||
|
must appear here too. Its sidecar pins are Renovate-managed and gated against this repo's own compose files, so the
|
||||||
quick start stays a topology CI has actually run.
|
quick start stays a topology CI has actually run.
|
||||||
It is its own job for two reasons: the images are already pushed and irreversible by then, so a Hub
|
It is its own job for two reasons: the images are already pushed and irreversible by then, so a Hub
|
||||||
outage leaves the promotion green and the images untouched; and the page has its own door — run the
|
outage leaves the promotion green and the images untouched; and the page has its own door — run the
|
||||||
@@ -1666,6 +1667,16 @@ the one-shot bootstrap) — and mounts no source. Secrets come from the environm
|
|||||||
running insecure. Before going live, supply the production secrets and any SSO credentials — the
|
running insecure. Before going live, supply the production secrets and any SSO credentials — the
|
||||||
**only** manual prep ([What you must supply](#what-you-must-supply-the-only-manual-prep)).
|
**only** manual prep ([What you must supply](#what-you-must-supply-the-only-manual-prep)).
|
||||||
|
|
||||||
|
**From the published image**, without a clone: save the `compose.yml` the
|
||||||
|
[Docker Hub page](https://hub.docker.com/r/larvit/plainpages) shows, then extract the Ory config the
|
||||||
|
image ships and start, with `X.Y.Z` the release you deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm larvit/plainpages:X.Y.Z tar -cf - ory | tar -xf -
|
||||||
|
mkdir -p plugins
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
**Back up the `pgdata` volume.** Once a plugin declares [storage](#plugin-storage), Postgres holds
|
**Back up the `pgdata` volume.** Once a plugin declares [storage](#plugin-storage), Postgres holds
|
||||||
business data that exists nowhere else, alongside Ory's identities — the stack stops being
|
business data that exists nowhere else, alongside Ory's identities — the stack stops being
|
||||||
reproducible from the image and config alone. Snapshot the volume, or `pg_dump` each database on a
|
reproducible from the image and config alone. Snapshot the volume, or `pg_dump` each database on a
|
||||||
|
|||||||
@@ -192,3 +192,20 @@ Read most (opens and searches):
|
|||||||
- `ci.sh` 1
|
- `ci.sh` 1
|
||||||
- `examples/plugins/admin/admin-clients.test.ts` 1
|
- `examples/plugins/admin/admin-clients.test.ts` 1
|
||||||
- `examples/plugins/admin/admin-grants.test.ts` 1
|
- `examples/plugins/admin/admin-grants.test.ts` 1
|
||||||
|
|
||||||
|
## Reading cost, chunk 4a8c9037acde, finished 2026-10-31T10:07:56Z
|
||||||
|
|
||||||
|
126616 new input tokens before the first commit.
|
||||||
|
|
||||||
|
Read most (opens and searches):
|
||||||
|
|
||||||
|
- `src/http/app-admin.test.ts` 7
|
||||||
|
- `examples/plugins/admin/admin-groups.ts` 4
|
||||||
|
- `README.md` 3
|
||||||
|
- `todo.md` 3
|
||||||
|
- `AGENTS.md` 2
|
||||||
|
- `examples/plugins/admin/README.md` 2
|
||||||
|
- `examples/plugins/admin/admin-grants.ts` 2
|
||||||
|
- `examples/plugins/admin/i18n/en-US.ts` 2
|
||||||
|
- `CHANGELOG.md` 1
|
||||||
|
- `examples/plugins/admin/admin-groups.test.ts` 1
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { test } from "node:test";
|
||||||
|
|
||||||
|
test("auto-release's checkout persists no credentials, so the tag push authenticates as renovate-bot", () => {
|
||||||
|
const workflow = readFileSync(".gitea/workflows/renovate.yml", "utf8");
|
||||||
|
const job = workflow.slice(workflow.indexOf("\n auto-release:"));
|
||||||
|
const checkout = job.slice(job.indexOf("actions/checkout@"), job.indexOf("\n - name:"));
|
||||||
|
assert.match(checkout, /^\s+persist-credentials: false$/m);
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { test } from "node:test";
|
import { test } from "node:test";
|
||||||
import { jwtFrom, leftoverPlaceholders, renderOverview } from "./dockerhub-overview.ts";
|
import { DESCRIPTION, jwtFrom, leftoverPlaceholders, renderOverview } from "./dockerhub-overview.ts";
|
||||||
|
|
||||||
const TEMPLATE = "release-tooling/dockerhub-overview.md.tmpl";
|
const TEMPLATE = "release-tooling/dockerhub-overview.md.tmpl";
|
||||||
const template = () => readFileSync(TEMPLATE, "utf8");
|
const template = () => readFileSync(TEMPLATE, "utf8");
|
||||||
@@ -46,3 +46,24 @@ test("jwtFrom accepts only a non-empty string token, never throwing on a hostile
|
|||||||
assert.equal(jwtFrom({ token: "" }), null);
|
assert.equal(jwtFrom({ token: "" }), null);
|
||||||
assert.equal(jwtFrom({ token: 42 }), null);
|
assert.equal(jwtFrom({ token: 42 }), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the short description opens a README sentence and fits Docker Hub's 100 characters", () => {
|
||||||
|
const readme = readFileSync("README.md", "utf8").replaceAll("**", "");
|
||||||
|
assert.ok(DESCRIPTION.length <= 100, `${DESCRIPTION.length} characters`);
|
||||||
|
const escaped = DESCRIPTION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
assert.match(readme, new RegExp(`^${escaped}(?=[\\s.:,])`, "m"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("every command the overview shows, the README shows too", () => {
|
||||||
|
const commands = (source: string) =>
|
||||||
|
[...source.matchAll(/```bash\n(.*?)```/gs)].flatMap((block) =>
|
||||||
|
(block[1] ?? "")
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => (line.split(" #")[0] ?? "").trim().replace(/larvit\/plainpages:\S+/g, "larvit/plainpages:X.Y.Z"))
|
||||||
|
.filter((line) => line !== ""),
|
||||||
|
);
|
||||||
|
const shown = new Set(commands(readFileSync("README.md", "utf8")));
|
||||||
|
const published = commands(template());
|
||||||
|
assert.ok(published.length > 0, `${TEMPLATE} should show commands`);
|
||||||
|
for (const command of published) assert.ok(shown.has(command), `${TEMPLATE} runs ${command}, which README.md doesn't show`);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Publishes the Docker Hub repository overview from dockerhub-overview.md.tmpl, rendering
|
// Publishes the Docker Hub repository overview from dockerhub-overview.md.tmpl, rendering
|
||||||
// `{{VERSION}}` to the release being published.
|
// `{{VERSION}}` to the release being published, and the short description.
|
||||||
|
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -8,6 +8,8 @@ const HUB = "https://hub.docker.com/v2";
|
|||||||
const TIMEOUT_MS = 30_000;
|
const TIMEOUT_MS = 30_000;
|
||||||
const VERSION = /^\d+\.\d+\.\d+$/;
|
const VERSION = /^\d+\.\d+\.\d+$/;
|
||||||
|
|
||||||
|
export const DESCRIPTION = "A self-hostable foundation for server-rendered web applications";
|
||||||
|
|
||||||
export function renderOverview(source: string, version: string): string {
|
export function renderOverview(source: string, version: string): string {
|
||||||
return source.replaceAll("{{VERSION}}", version);
|
return source.replaceAll("{{VERSION}}", version);
|
||||||
}
|
}
|
||||||
@@ -81,21 +83,21 @@ async function main(): Promise<number> {
|
|||||||
if (!jwt) return fail("Docker Hub login returned no token");
|
if (!jwt) return fail("Docker Hub login returned no token");
|
||||||
|
|
||||||
const res = await post(`${HUB}/repositories/${repo}/`, {
|
const res = await post(`${HUB}/repositories/${repo}/`, {
|
||||||
body: JSON.stringify({ full_description: body }),
|
body: JSON.stringify({ description: DESCRIPTION, full_description: body }),
|
||||||
headers: { authorization: `Bearer ${jwt}`, "content-type": "application/json" },
|
headers: { authorization: `Bearer ${jwt}`, "content-type": "application/json" },
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
});
|
});
|
||||||
if ("error" in res) return fail(`Docker Hub unreachable: ${res.error}`);
|
if ("error" in res) return fail(`Docker Hub unreachable: ${res.error}`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return fail(
|
return fail(
|
||||||
`Docker Hub overview PATCH failed: ${res.status} ${res.text}` +
|
`Docker Hub description and overview PATCH failed: ${res.status} ${res.text}` +
|
||||||
(res.status === 403
|
(res.status === 403
|
||||||
? "\n403 means DOCKERHUB_TOKEN lacks the delete scope — editing the overview needs " +
|
? "\n403 means DOCKERHUB_TOKEN lacks the delete scope — editing the overview needs " +
|
||||||
"read/write/delete, which pushing images alone does not (README -> CI/CD)."
|
"read/write/delete, which pushing images alone does not (README -> CI/CD)."
|
||||||
: ""),
|
: ""),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
process.stdout.write(`Docker Hub overview updated for ${repo} at ${version}\n`);
|
process.stdout.write(`Docker Hub description and overview updated for ${repo} at ${version}\n`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
|
|
||||||
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
| 1 | 0.4.21 | | owner | **Publish every release to Docker Hub, and give the Hub page its short description.** | 3 | 3 | 2 | 6 | 5 | 1 by 3 | 1.3 |
|
|
||||||
| 32 | 0.4.21 | | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 4 | 2 | 2 | 6 | 7 | code health by 2 | 1.1 |
|
| 32 | 0.4.21 | | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 4 | 2 | 2 | 6 | 7 | code health by 2 | 1.1 |
|
||||||
| 13 | 0.4.21 | | | **Accept a verification code pasted with surrounding whitespace.** | 4 | 2 | 3 | 7 | 7 | 2 by 8 | 1.0 |
|
| 13 | 0.4.21 | | | **Accept a verification code pasted with surrounding whitespace.** | 4 | 2 | 3 | 7 | 7 | 2 by 8 | 1.0 |
|
||||||
| 19 | 0.5.0 | | question | **Run the documented Docker commands on macOS and fix whatever misbehaves.** | 2 | 2 | 1 | 3 | 3 | 1 by 3 | 3.3 |
|
| 19 | 0.5.0 | | question | **Run the documented Docker commands on macOS and fix whatever misbehaves.** | 2 | 2 | 1 | 3 | 3 | 1 by 3 | 3.3 |
|
||||||
@@ -28,17 +27,6 @@
|
|||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
### 1. Publish every release to Docker Hub, and give the Hub page its short description.
|
|
||||||
|
|
||||||
Hub's newest tag is 0.4.0, while git has v0.4.1 onwards, and the overview still says 0.4.0. No
|
|
||||||
`release.yml` run exists for the tags auto-release pushes as `renovate-bot`; find out why Gitea
|
|
||||||
doesn't fire `push: tags` for them, fix it, and backfill the missing tags. fejkdata did this
|
|
||||||
(`../fejkdata`, commits `51d924a` and `34c8234`), and two parts port directly: a one-line
|
|
||||||
description riding the existing PATCH as its `description` field, and a unit test that the
|
|
||||||
description opens a README sentence and every command the overview shows appears in the README.
|
|
||||||
fejkdata also syncs the page on any push to `main`, with its own token in an environment only `main`
|
|
||||||
may use; decide whether that fits here, where `{{VERSION}}` must render a released tag.
|
|
||||||
|
|
||||||
### 32. Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.
|
### 32. Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.
|
||||||
|
|
||||||
Nothing checks README → "Two browser floors", and no E2E engine is old enough to show a break.
|
Nothing checks README → "Two browser floors", and no E2E engine is old enough to show a break.
|
||||||
|
|||||||
Reference in New Issue
Block a user