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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
group: ci-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
check:
Expand Down
38 changes: 37 additions & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ on:
tag:
required: true
type: string
mode:
description: >-
canonical when the calling Release run verified this exact tag with the
complete source check; mirror for a current-main retry, which reruns it.
required: false
default: mirror
type: string

permissions:
contents: read
Expand Down Expand Up @@ -63,8 +70,10 @@ jobs:
id: identity
env:
REQUESTED_TAG: ${{ inputs.tag }}
RELEASE_MODE: ${{ inputs.mode }}
run: |
set -euo pipefail
[[ "$RELEASE_MODE" == canonical || "$RELEASE_MODE" == mirror ]]
[[ "$GITHUB_REPOSITORY" == hraness/direct && "$GITHUB_REPOSITORY_ID" == 1306913032 && "$GITHUB_ACTOR_ID" == 894119 ]]
[[ "$REQUESTED_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
git fetch --no-tags --unshallow origin "refs/heads/main:refs/remotes/origin/main"
Expand All @@ -79,6 +88,12 @@ jobs:
EXPECTED_VERSION="${REQUESTED_TAG#v}" EXPECTED_WORKFLOW_SHA="$workflow_sha" \
node "$GITHUB_WORKSPACE/scripts/github-release.ts" mirror "$canonical_directory"
[[ "$(node -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).sourceSha' "$canonical_directory/release-manifest.json")" == "$source_sha" ]]
if [[ "$RELEASE_MODE" == canonical ]]; then
# The complete source check below is reused only from this exact Release
# run: its Verify job checked the same tag commit before attestation.
[[ "$GITHUB_EVENT_NAME" == push && "$GITHUB_REF" == "refs/tags/$REQUESTED_TAG" && "$GITHUB_SHA" == "$source_sha" ]]
[[ "$(node -p 'String(JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).runId)' "$canonical_directory/release-manifest.json")" == "$GITHUB_RUN_ID" ]]
fi
source_tree="$RUNNER_TEMP/direct-mirror-source"
git worktree add --detach "$source_tree" "$source_sha"
printf 'source_sha=%s\nworkflow_sha=%s\nsource_tree=%s\ncanonical_directory=%s\n' \
Expand Down Expand Up @@ -118,7 +133,9 @@ jobs:
printf 'already_public=%s\n' "$already_public" >> "$GITHUB_OUTPUT"
- run: bun install --frozen-lockfile --ignore-scripts
working-directory: ${{ steps.identity.outputs.source_tree }}
- run: bun run check
- name: Complete source check (mirror retries only)
if: inputs.mode != 'canonical'
run: bun run check
working-directory: ${{ steps.identity.outputs.source_tree }}
- name: Verify generated tree
working-directory: ${{ steps.identity.outputs.source_tree }}
Expand Down Expand Up @@ -756,13 +773,32 @@ jobs:
EXPECTED_VERSION="$package_version" EXPECTED_WORKFLOW_SHA="$WORKFLOW_SHA" \
node "$GITHUB_WORKSPACE/scripts/github-release.ts" mirror-verify "$source_directory"
mkdir -p "$registry_directory"
# A fresh version can take a minute to reach every registry replica, and
# npm caches packuments, so poll with --prefer-online for up to 5 minutes.
# Replace this poll with the shared npm-visible action once it exists.
registry_poll_err="$RUNNER_TEMP/registry-poll.err"
for attempt in $(seq 1 20); do
if [[ "$(npm view "$package_spec" version --prefer-online \
--registry=https://registry.npmjs.org 2>"$registry_poll_err")" == "$package_version" ]]; then
break
fi
if [[ "$attempt" == 20 ]]; then
echo "::error::$package_spec did not become visible on the npm registry"
echo "Last npm view stderr:"
cat "$registry_poll_err" || true
exit 1
fi
sleep 15
done
npm pack "$package_spec" \
--prefer-online \
--ignore-scripts \
--json \
--pack-destination "$registry_directory" \
--registry=https://registry.npmjs.org \
> "$registry_pack_json"
npm view "$package_spec" name version dist \
--prefer-online \
--json \
--registry=https://registry.npmjs.org \
> "$registry_view_json"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ jobs:
uses: ./.github/workflows/npm-publish.yml
with:
tag: ${{ needs.authorize.outputs.tag }}
mode: ${{ needs.authorize.outputs.mode }}

legacy:
name: Recover historical GitHub Release
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
- State proof limits precisely. Fixture evidence does not prove the live adapter, service, host, operating system, or device behavior that the composition replaces. In public copy, state each limit once, beside the claim it limits, in the reader's terms (`STYLE.md`).
- Use focused local checks and independent impact review, then require complete CI on the exact current head, authoritative current `main`, and checked integration tree as the final source aggregate. CI must run the complete root `bun run check`, preserve committed-output cleanliness, and pass both `check` and `Required` in the same run attempt. Compare workflow, command, discovery, deadline, and platform changes against the prior required coverage; keep the coverage contracts passing. Record source, lockfile, toolchain, and run identity. Keep `bun run check` available as the complete local fallback when coverage or equivalence is uncertain, and diagnose known failures before requalification. This source receipt does not replace explicit local, native, browser, install, release, npm-mirror, or provider acceptance. See `CONTRIBUTING.md` for the final receipt. Run the todo example's production build and marker scan when changing the example or package boundaries.
- Run the React Native example's iOS, Android, and web export gate when changing mobile integration or production boundaries.
- Follow `docs/publishing.md` for canonical GitHub releases and optional exact-archive npm mirrors. Treat one protected lightweight `v*` tag on `main` as the complete release request. Restrict version-tag creation to organization administrators and block updates/deletion with no bypass actors. Keep the complete source gate and isolated installation before checkout-free four-subject attestation and immutable five-asset publication. Bind verified certificate, source, current helper closure, original run/attempt, and actual remote bytes; preserve draft IDs and reconcile uncertain writes before retrying. npm runs only after all required canonical jobs succeed. Within the npm workflow, only its minimal dependent publication job may request OIDC; rebind the downloaded exact artifact, every remote stable tag, and current `main` immediately before publication. Preserve the full mirror source gate and registry identity/install readback. Current-main mirror retries retain the original canonical source and provenance. Historical asset-free recovery is restricted to versions at or below 0.7.20: bind current helpers to reviewed Git blobs and invoke them against exact tagged source with config/environment loading disabled and `npm pack --ignore-scripts`, so recovery never runs a historical `prepack`. Never move a tag or replace a package version or immutable artifact. Record canonical and mirror delivery outcomes separately.
- Follow `docs/publishing.md` for canonical GitHub releases and optional exact-archive npm mirrors. Treat one protected lightweight `v*` tag on `main` as the complete release request. Restrict version-tag creation to organization administrators and block updates/deletion with no bypass actors. Keep the complete source gate and isolated installation before checkout-free four-subject attestation and immutable five-asset publication. Bind verified certificate, source, current helper closure, original run/attempt, and actual remote bytes; preserve draft IDs and reconcile uncertain writes before retrying. npm runs only after all required canonical jobs succeed. Within the npm workflow, only its minimal dependent publication job may request OIDC; rebind the downloaded exact artifact, every remote stable tag, and current `main` immediately before publication. Preserve the full mirror source gate on current-main mirror retries; a canonical tag run may reuse only its own Verify job's complete check of the identical tag commit. Preserve registry identity/install readback. Current-main mirror retries retain the original canonical source and provenance. Historical asset-free recovery is restricted to versions at or below 0.7.20: bind current helpers to reviewed Git blobs and invoke them against exact tagged source with config/environment loading disabled and `npm pack --ignore-scripts`, so recovery never runs a historical `prepack`. Never move a tag or replace a package version or immutable artifact. Record canonical and mirror delivery outcomes separately.

<!-- hraness-public-copy:start -->
- Public copy (websites, READMEs, docs, package and GitHub descriptions, CLI help, `llms.txt`, generated pages) follows `STYLE.md`, synced from hraness/.github. Text a model writes for publication also follows `GENERATION_STYLE.md`.
Expand Down
14 changes: 9 additions & 5 deletions docs/publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,12 @@ job failed remains usable only when all four canonical jobs succeeded.
The read-only mirror job verifies the immutable GitHub record, all five assets,
actual bytes, and cryptographic provenance. It keeps the canonical source `C`
separate from protected current workflow `W`, checks out `C` in an isolated tree,
and retains the frozen install, complete `bun run check`, generated cleanliness,
and exact-archive isolated installation. Current helpers are bound to `W` before
running against `C`. It copies exactly the tarball, `npm-pack.json`, and
and retains the frozen install, generated cleanliness, and exact-archive isolated
installation. A current-main mirror retry also reruns the complete `bun run check`
on `C`. A canonical tag run instead reuses the complete check its own **Verify**
job ran on the same tag commit; the mirror job requires that the release manifest
names this run and that `C` is the pushed tag commit before it skips the rerun.
Current helpers are bound to `W` before running against `C`. It copies exactly the tarball, `npm-pack.json`, and
`npm-package.sha256` into the privileged handoff; it never repacks the mirror.

Within the npm workflow, the only job with OIDC authority checks out no source
Expand All @@ -159,8 +162,9 @@ The candidate must remain the newest remote stable tag and be newer than every
published stable npm version. Fresh main/tag reads immediately precede the exact
`npm publish --ignore-scripts --provenance` to `https://registry.npmjs.org`.

The read-only registry job compares the canonical archive with the public npm
package by complete extracted safe path, entry type, mode, size, and regular-file
The read-only registry job first waits up to five minutes, reading past the npm
metadata cache, for the new version to become visible. It then compares the
canonical archive with the public npm package by complete extracted safe path, entry type, mode, size, and regular-file
SHA-256/SHA-512. Each transport must independently match its npm SHA-1/SHA-512 and
registry metadata, since registry compression can differ. It then installs that
registry archive in clean Bun/npm consumers. An existing npm version is left
Expand Down
2 changes: 1 addition & 1 deletion examples/todos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ bun run example:dev
bun run example:direct
```

Use Node 24 and the frozen repository dependencies. Each command compiles one immutable StyleX generation with the pinned Vite 8.2.1/Rolldown 1.2.8 toolchain, then serves it on `127.0.0.1:5173`. Run only one preview at a time. Production opens at `/`; the workbench opens at `/direct/`. After editing a recipe or component, stop the command, rebuild/restart it, and refresh the browser. This compiled preview does not claim HMR or React-plugin support. Ctrl-C closes the owned Vite preview server.
Use Node 24 and the frozen repository dependencies. Each command compiles one immutable StyleX generation with the pinned Vite 8.2.1/Rolldown 1.2.8 toolchain, then serves it on `127.0.0.1:5173`. Set `DIRECT_EXAMPLE_PORT` to serve on another port, or to `0` for a free one; the command prints the address it bound. Run only one preview per port. Production opens at `/`; the workbench opens at `/direct/`. After editing a recipe or component, stop the command, rebuild/restart it, and refresh the browser. This compiled preview does not claim HMR or React-plugin support. Ctrl-C closes the owned Vite preview server.

The workbench provides three stable scenarios:

Expand Down
18 changes: 16 additions & 2 deletions examples/todos/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,29 @@ import { preview } from "vite";
import { buildTodo } from "./build.ts";
import { parseTodoBuildTarget } from "./build-contract.ts";

const defaultPort = 5173;

// DIRECT_EXAMPLE_PORT lets parallel worktrees serve side by side; 0 picks a free port.
function parsePreviewPort(value: string | undefined): number {
if (value === undefined || value === "") return defaultPort;
if (!/^(0|[1-9][0-9]{0,4})$/u.test(value) || Number(value) > 65_535) {
throw new Error(`DIRECT_EXAMPLE_PORT must be an integer from 0 to 65535, got ${JSON.stringify(value)}`);
}
return Number(value);
}

const target = parseTodoBuildTarget(process.argv[2]);
const port = parsePreviewPort(process.env.DIRECT_EXAMPLE_PORT);
const directory = await buildTodo(target);
const server = await preview({
configFile: false,
root: directory,
build: { outDir: directory },
preview: { host: "127.0.0.1", port: 5173, strictPort: true, open: false },
preview: { host: "127.0.0.1", port, strictPort: true, open: false },
});
console.log(`Compiled Todo preview: http://127.0.0.1:5173/${target === "direct" ? "direct/" : ""}`);
const address = server.httpServer.address();
const boundPort = typeof address === "object" && address !== null ? address.port : port;
console.log(`Compiled Todo preview: http://127.0.0.1:${String(boundPort)}/${target === "direct" ? "direct/" : ""}`);
console.log(`Generation: ${directory}\nAfter edits, stop this command, rebuild/restart, then refresh the browser. HMR is disabled.`);
let closing = false;
function close(): void {
Expand Down
9 changes: 7 additions & 2 deletions scripts/ci-source-coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const releaseWorkflow = record(Bun.YAML.parse(readFileSync(new URL("../.github/w
const canonicalPackScript = 'set -euo pipefail\npackage_directory="$(mktemp -d "$RUNNER_TEMP/direct-canonical-ci.XXXXXX")"\nbun --no-env-file --config=/dev/null run ./scripts/prepare-npm-package.ts "$package_directory"\ncat "$package_directory/npm-pack.json"\n';
const canonicalPackStep = " - name: Verify canonical npm archive\n run: |\n"
+ canonicalPackScript.trimEnd().split("\n").map((line) => ` ${line}\n`).join("");
const perCommitConcurrency = "concurrency:\n group: ci-${{ github.event_name == 'pull_request' && github.ref || github.sha }}\n cancel-in-progress: ${{ github.event_name == 'pull_request' }}\n";
const priorConcurrency = "concurrency:\n group: ci-${{ github.ref }}\n cancel-in-progress: true\n";
const phases = [
"typecheck", "check:effect", "test:npm-release", "build", "test:package", "test", "lint",
"example:test", "example:typecheck", "example:verify", "example:react-native:test",
Expand Down Expand Up @@ -101,8 +103,11 @@ function assertSourceCoverage(packageValue: RecordValue, workflowValue: RecordVa

test("complete CI retains the root aggregate, release-contract discovery, and committed-output checks", () => {
const current = readFileSync(new URL("../.github/workflows/ci.yml", import.meta.url), "utf8");
// Removing only this additive pack gate and its Node patch pin must recover all prior CI bytes.
const prior = current.replace(canonicalPackStep, "").replace('node-version: "24.18.1"', 'node-version: "24"');
// Removing only this additive pack gate, its Node patch pin, and the per-commit main
// concurrency (PR runs still cancel superseded heads) must recover all prior CI bytes.
expect(current).toContain(perCommitConcurrency);
const prior = current.replace(canonicalPackStep, "").replace('node-version: "24.18.1"', 'node-version: "24"')
.replace(perCommitConcurrency, priorConcurrency);
expect(createHash("sha256").update(prior).digest("hex")).toBe("b47f2d0ead76414025eddf15dedbd940ba8114a53fc93b9efecc78554015aaa0");
assertSourceCoverage(manifest, workflow);
});
Expand Down
Loading
Loading