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
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: CI

on:
pull_request:
push:
branches: [main, develop]

# Cancel superseded runs on the same ref: only the latest push matters.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
verify:
name: Typecheck, lint, test, build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v6

- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm

# Fails loudly if the lockfile has drifted, which is the point of pnpm here.
- run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm exec tsc --noEmit

- name: Lint
run: pnpm lint

- name: Test
run: pnpm test

- name: Build
run: pnpm build

# The entry bundle is what every visitor pays for on first load. If a
# heavy dependency escapes a lazy route chunk, this is where we find out
# rather than in production.
- name: Check entry bundle budget
run: |
BUDGET=92160
ENTRY=$(find dist/assets -name 'index-*.js' -print -quit)
SIZE=$(gzip -c "$ENTRY" | wc -c | tr -d ' ')
echo "entry: $ENTRY"
echo "gzipped: ${SIZE}B budget: ${BUDGET}B"
if [ "$SIZE" -gt "$BUDGET" ]; then
echo "::error::Entry bundle ${SIZE}B exceeds the ${BUDGET}B budget."
exit 1
fi
73 changes: 73 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Deploy

# Deploys a TAG, never a branch: whatever shipped is always reachable by
# version. Normally started by the Release workflow after it tags a merge to
# main; a hand-pushed tag works too.
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: 'Tag to deploy, e.g. v0.2.0'
required: true
type: string

concurrency:
group: deploy-production
cancel-in-progress: false

permissions:
contents: read

jobs:
deploy:
name: Build and deploy to Cloudflare
runs-on: ubuntu-latest
environment:
name: production
url: https://devtools.fadeltd.dev
steps:
- uses: actions/checkout@v7
with:
# Deploy exactly the tagged commit, whichever way this was started.
ref: ${{ inputs.tag || github.ref }}

- uses: pnpm/action-setup@v6

- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

# Cloudflare would happily serve a broken build, so nothing ships that
# has not passed the same gate CI applies to pull requests.
- name: Typecheck
run: pnpm exec tsc --noEmit

- name: Lint
run: pnpm lint

- name: Test
run: pnpm test

- name: Build
run: pnpm build

- name: Deploy
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy

- name: Smoke check the live site
run: |
sleep 10
for path in / /id-gen /json /diff /base64 /list /text-stats; do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "https://devtools.fadeltd.dev${path}")
echo "${path} -> ${code}"
[ "${code}" = "200" ] || { echo "::error::${path} returned ${code}"; exit 1; }
done
88 changes: 88 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Release

# Merging to main does not deploy directly. It derives the next version from
# the [Unreleased] section of CHANGELOG.md, rewrites the changelog, commits
# that back to main, and pushes a tag. The TAG is what deploys.
#
# A merge with an empty [Unreleased] releases nothing, so merging and releasing
# stay separate decisions.
on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: release
cancel-in-progress: false

permissions:
contents: write
actions: write

jobs:
release:
name: Cut a release from the changelog
runs-on: ubuntu-latest
# Never react to our own release commit.
if: "!contains(github.event.head_commit.message, '[skip ci]')"
env:
# A tag pushed with GITHUB_TOKEN does NOT trigger other workflows, by
# design, to prevent recursion. With a PAT in RELEASE_TOKEN the tag
# triggers Deploy natively; without one we start Deploy explicitly.
HAS_PAT: ${{ secrets.RELEASE_TOKEN != '' }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}

- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile

- id: cut
name: Derive the version from CHANGELOG.md
run: pnpm exec vite-node scripts/release.ts

- name: Commit the changelog and tag
if: steps.cut.outputs.released == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md package.json
# [skip ci] so this push cannot re-enter this workflow when a PAT is
# in use -- with a PAT, pushes DO trigger workflows.
git commit -m "Release ${{ steps.cut.outputs.tag }} [skip ci]"
git tag -a "${{ steps.cut.outputs.tag }}" -m "${{ steps.cut.outputs.tag }}"
git push origin HEAD:main
git push origin "${{ steps.cut.outputs.tag }}"

- name: Create the GitHub release
if: steps.cut.outputs.released == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.cut.outputs.tag }}" \
--title "${{ steps.cut.outputs.tag }}" \
--notes "${{ steps.cut.outputs.notes }}"

# Only needed while no PAT is configured; with one, the tag push above
# has already started Deploy.
- name: Start Deploy (no PAT configured)
if: steps.cut.outputs.released == 'true' && env.HAS_PAT != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "::notice::No RELEASE_TOKEN set, so the tag cannot trigger Deploy. Dispatching it directly."
gh workflow run deploy.yml --ref main -f tag="${{ steps.cut.outputs.tag }}"

- name: Summary
run: |
if [ "${{ steps.cut.outputs.released }}" = "true" ]; then
echo "Released ${{ steps.cut.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY"
else
echo "No [Unreleased] entries — nothing released." >> "$GITHUB_STEP_SUMMARY"
fi
32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,37 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

Nothing yet.
### Added
- **ID Generator** — prefixed identifiers in the Stripe style
(`sk_live_` plus 24 random characters): set a prefix, separator, length,
alphabet and batch size. Doubles as a password generator via a symbol
alphabet and an
option to drop confusable glyphs (`0`/`O`, `1`/`l`/`I`).
- Live entropy readout with a qualitative strength verdict. Deliberately no
"time to crack" figure: that depends entirely on assumed hardware and on
whether the value is hashed, so quoting one would be false precision.
- Presets for Stripe secret and test keys, object ids, API tokens, hex
session ids, passwords and human-readable codes.
- Values come from `crypto.getRandomValues` with rejection sampling, never
`value % n`, which biases toward the start of the alphabet.
- Settings persist; **generated values never do**.
- Collapsible sidebar, kept as an icon rail with accessible names intact.
- Link to the GitHub repository in the header.
- Continuous integration on every pull request, including an entry-bundle size
budget so a heavy dependency escaping a lazy chunk fails the build rather
than reaching production.
- Release automation: merging to `main` creates a version tag, and the **tag**
is what deploys. A merge that does not change the version in `package.json`
releases nothing, so merging and releasing are separate decisions.

### Fixed
- Focus rings on full-bleed text areas were drawn outside the element and
clipped by the surrounding pane, so only the top and right edges were
visible. They are now drawn inset.

### Changed
- Unbiased randomness primitives moved to `src/lib/random.ts`, now shared by
the list shuffler and the ID generator.

## [0.1.0] — 2026-09-24

Expand Down
43 changes: 43 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,49 @@ Small, focused PRs. If you are planning something large, open an issue first —
the project has a deliberate list of things it will not do, and it would be a
shame for you to build one of them.

## Releases

**You never pick a version number, and you never write a release date.**

While you work, add your entry under `## [Unreleased]` in `CHANGELOG.md`, using
a Keep a Changelog heading:

```markdown
## [Unreleased]

### Added
- The thing you added
```

Merging to `main` then does the rest, automatically:

1. Reads `[Unreleased]` and derives the semver bump from its headings —
`### Breaking` → major, `### Added` → minor, anything else
(`Fixed`, `Changed`, `Security`, `Removed`) → patch.
2. Rewrites `CHANGELOG.md`, moving `[Unreleased]` into a dated
`## [x.y.z]` section and leaving a fresh empty `[Unreleased]` behind.
3. Bumps `version` in `package.json`, commits that back to `main`, and pushes
the tag `vx.y.z`.
4. **The tag** triggers the deploy to Cloudflare. Merging alone never deploys.

An empty `[Unreleased]` releases nothing, so a docs-only or refactor merge ships
nothing. That is deliberate: merging and releasing are separate decisions.

Two notes for anyone editing the workflows:

- `### Removed` is deliberately a *patch*, not a major. Inferring a major bump
from a tidy-up would let a cleanup silently become a 1.0. Major requires an
explicit `### Breaking` heading.
- Below 1.0, a breaking change bumps the minor (`0.4.2` → `0.5.0`), per semver
convention for pre-stable projects.

The bump logic lives in `scripts/changelog.ts` and is unit-tested, because a
mistake there silently ships the wrong version. Preview what a merge would do:

```bash
pnpm exec vite-node scripts/release.ts --dry-run
```

## Licence

By contributing you agree your contributions are licensed under the MIT
Expand Down
Loading
Loading