407c666789
`brand-drift` is a BLOCKING context and branch protection here is `['*']`, so a
red run in this job jams every merge in the repo — and the repair would have to
travel through the CI it just jammed. That asymmetry is the whole reason to
harden it ahead of the molecule-ai -> enteros org move (internal#1089).
What I measured first, because the obvious story turned out to be wrong:
`actions/checkout` DOES follow Gitea's transfer 301. I transferred a throwaway
probe repo out of molecule-ai and re-ran a checkout of its OLD owner path: it
resolved and the job was green. So the move alone does not break this line.
What DID break it was the destination org: `enteros` was created
`visibility: private`, and a private org hides even its public repos from
anonymous access. The same probe checkout went RED (`git clone` failed, the API
answered 404 to an anonymous caller through the edge) purely because of that.
Setting the org public turned the identical job green again. 80 of the 110
repos being moved are public, and this job fetches the SSOT token-lessly, so
that setting is load-bearing for the whole move, not just here.
The residual risks the redirect does not cover are why this is now a candidate
list rather than one string:
* the 301 exists only while the SOURCE org is kept, and a new repo reclaiming
a transferred name under molecule-ai silently SHADOWS the redirect;
* a repo rename (molecule-ai-sdk -> enteros-sdk) is still planned on top;
* the org-visibility regression above can recur.
So: try each candidate spelling, take the first that clones, and if NONE
answer, emit `::warning::` and skip rather than post red. It never claims green
by silence — a candidate that resolves is still checked strictly, byte-for-byte,
and both the check and the re-sync are gated on having actually resolved one.
Verified on this Gitea against a real cross-org transfer, in a workflow with the
same shape as this one: the resolver is green before the transfer, green after
it, green with the candidate order REVERSED (so it is genuinely falling back and
not just getting lucky on entry one), and green-with-a-warning when every
candidate is bogus.
Refs internal#1089
175 lines
6.8 KiB
YAML
175 lines
6.8 KiB
YAML
# Enter OS landing site — CI gate.
|
|
#
|
|
# Everything a Next.js static-ish marketing site can be wrong about, checked
|
|
# before merge. Two independent contexts so a red one names its own failure:
|
|
#
|
|
# build npm ci -> next build -> tsc --noEmit -> node:test
|
|
# `next build` runs FIRST because the content-collections Next plugin
|
|
# is what emits .content-collections/generated (the `content-collections`
|
|
# module alias) and .next/types — without it `tsc --noEmit` reports
|
|
# ~20 phantom TS2307/TS7006 errors. Build, then typecheck.
|
|
# lint npm ci -> eslint
|
|
# `next lint` is deprecated in Next 15.5 and removed in 16, and was
|
|
# never configured in this repo (no eslint config, no eslint dep), so
|
|
# it silently did nothing. ESLint now runs directly off
|
|
# eslint.config.mjs (next/core-web-vitals + next/typescript).
|
|
# Errors fail the job; the repo's pre-existing `no-img-element`
|
|
# findings stay warnings (this is a verbatim DOM port of a static
|
|
# design that intentionally uses plain <img>).
|
|
# brand-drift
|
|
# re-run the @enteros/brand sync and fail if any committed brand asset
|
|
# differs from the SSOT. Mirrors molecule-ai-sdk's contracts-codegen-
|
|
# drift pattern: generated/copied artifacts are committed, so the only
|
|
# thing keeping them honest is a job that regenerates them and diffs.
|
|
# Node-only, no npm install, ~2s.
|
|
#
|
|
# Branch protection on main requires every context here to be green.
|
|
|
|
name: ci
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
concurrency:
|
|
group: ci-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
NEXT_TELEMETRY_DISABLED: "1"
|
|
|
|
jobs:
|
|
build:
|
|
name: build
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
# Matches the Dockerfile's node:22-bookworm-slim build stage.
|
|
node-version: "22"
|
|
cache: npm
|
|
|
|
- name: Install from lockfile
|
|
run: npm ci
|
|
|
|
- name: Build (emits content-collections bindings + .next/types)
|
|
run: npm run build
|
|
|
|
- name: Typecheck
|
|
run: npx tsc --noEmit
|
|
|
|
- name: Tests
|
|
run: npm test
|
|
|
|
lint:
|
|
name: lint
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "22"
|
|
cache: npm
|
|
|
|
- name: Install from lockfile
|
|
run: npm ci
|
|
|
|
- name: ESLint
|
|
run: npm run lint
|
|
|
|
brand-drift:
|
|
name: brand-drift
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# The brand SSOT. It is a public repo, so this needs no token — nothing
|
|
# to leak, nothing to rotate.
|
|
#
|
|
# WHY THIS IS A LOOP AND NOT `actions/checkout: repository:` ANY MORE.
|
|
# The SSOT repo is moving: org `molecule-ai` -> `enteros` (internal#1089),
|
|
# with a repo rename to follow. Gitea leaves a 301 on the old path, and a
|
|
# checkout DOES follow it — measured, against a real transferred probe
|
|
# repo on this instance. So the single hardcoded spelling is not broken
|
|
# by the move itself. What it is exposed to is everything AROUND the
|
|
# move: the redirect only exists while the source org is kept (and a new
|
|
# repo reclaiming the old name silently SHADOWS it), and the destination
|
|
# org must stay publicly visible or an anonymous fetch 404s — that one
|
|
# actually reproduced, turning this job red, because `enteros` was
|
|
# created `visibility: private`.
|
|
#
|
|
# This job is a BLOCKING context under `['*']` branch protection, so a
|
|
# red here jams every merge in the repo — and the repair would have to
|
|
# travel through the CI it just jammed. That asymmetry is why it resolves
|
|
# a candidate LIST and, if none answer, warns loudly and skips instead of
|
|
# posting red. It never asserts green by silence: a candidate that DOES
|
|
# resolve is checked strictly, and a total failure is a `::warning::`.
|
|
- name: Resolve the brand SSOT across candidate owner/name spellings
|
|
id: ssot
|
|
env:
|
|
BRAND_SSOT_CANDIDATES: >-
|
|
molecule-ai/molecule-ai-sdk
|
|
enteros/molecule-ai-sdk
|
|
enteros/enteros-sdk
|
|
molecule-ai/enteros-sdk
|
|
SERVER_URL: ${{ github.server_url }}
|
|
run: |
|
|
set -uo pipefail
|
|
srv="${SERVER_URL:-}"
|
|
if [ -z "$srv" ]; then
|
|
srv="$(git config --get remote.origin.url | sed -E 's#^(https?://[^/]+)/.*#\1#')"
|
|
fi
|
|
rm -rf .brand-ssot
|
|
for cand in $BRAND_SSOT_CANDIDATES; do
|
|
if git clone --depth 1 --quiet "$srv/$cand.git" .brand-ssot 2>/dev/null; then
|
|
echo "resolved=$cand" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::brand SSOT resolved at $cand"
|
|
exit 0
|
|
fi
|
|
rm -rf .brand-ssot
|
|
done
|
|
echo "resolved=" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::brand SSOT unreachable at every candidate ($BRAND_SSOT_CANDIDATES) — SKIPPING the brand drift check. This gate is not proving anything right now; fix the SSOT location before trusting a green run."
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
if: steps.ssot.outputs.resolved != ''
|
|
with:
|
|
node-version: "22"
|
|
|
|
# No `npm ci`: sync-brand.mjs is deliberately node-stdlib only, so this
|
|
# context stays fast and cannot be broken by a dependency.
|
|
- name: Brand assets match @enteros/brand byte-for-byte
|
|
if: steps.ssot.outputs.resolved != ''
|
|
env:
|
|
ENTEROS_BRAND_DIR: ${{ github.workspace }}/.brand-ssot/brand
|
|
run: npm run brand:check
|
|
|
|
# `brand:check` compares; `brand:sync` writes. Running the writer too
|
|
# proves the two agree — a check that passed while the sync would have
|
|
# rewritten files would be a check that means nothing.
|
|
- name: Re-running the sync produces no diff
|
|
if: steps.ssot.outputs.resolved != ''
|
|
env:
|
|
ENTEROS_BRAND_DIR: ${{ github.workspace }}/.brand-ssot/brand
|
|
run: |
|
|
set -euo pipefail
|
|
npm run brand:sync
|
|
if ! git diff --exit-code --stat; then
|
|
echo "::error::npm run brand:sync changed committed files. Commit the result."
|
|
exit 1
|
|
fi
|