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
115 changes: 70 additions & 45 deletions .github/workflows/build-publish.yml
Original file line number Diff line number Diff line change
@@ -1,62 +1,87 @@
name: Build and Publish to npm and unpkg
# Release: the merge of a release pull request (`just release-patch`: version
# bump, title `release widget-mapbox <version>`) publishes the package to npm, then
# sets the tag <version> (a bare version; a tag always means "on npm") and
# creates the GitHub release, both with the release GitHub App's token. Main
# accepts only pull requests whose required checks (ci-ok, security / report)
# passed, so every release comes from a merged, fully green commit; nothing is
# published or tagged from a laptop.
# The file keeps its old name because npm's trusted publisher of the package
# names it (IronFlock/widget-mapbox :: build-publish.yml).
# A no-release merge starts nothing: `paths` holds only the version file, and
# when a no-release pull request touches it anyway (a dependency) the jobs
# below are skipped by its merge title, so no runner starts.
# ci.yml calls it on release pull requests (workflow_call), and run by hand
# (workflow_dispatch) it is a dry run: `npm publish --dry-run`, no tag.
# A re-run is safe: a version already on npm is not published again.
name: release

on:
push:
tags:
- "*"
branches: [main]
paths: [package.json]
workflow_call:
workflow_dispatch:

# id-token lets the runner mint the OIDC token npm exchanges for publish rights.
# Without it `npm publish` looks for a token and fails.
permissions:
contents: write
id-token: write
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
npm:
if: github.event_name != 'push' || startsWith(github.event.head_commit.message, 'release ')
runs-on: ubuntu-latest

timeout-minutes: 15
permissions:
contents: read
# the OIDC token npm exchanges for publish rights (trusted publishing,
# bound to this file)
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v7

# Pinned, not "24": npm trusted publishing needs npm >= 11.5.1, and the
# npm bundled with a Node line is not something we want floating.
- name: Install Node.js
uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# npm >= 11.5.1 for trusted publishing; Node 24.20.0 bundles npm 11.19.0.
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "24.20.0"
registry-url: "https://registry.npmjs.org"

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

# No .npmrc, no NPM_TOKEN. npm detects the OIDC environment and
# authenticates against the trusted publisher configured for this
# package (IronFlock/widget-mapbox :: build-publish.yml).
- name: Publish to npm
# Skips a version that is already on npm: the first @ironflock release of
# each widget was published by hand (npm only accepts a trusted publisher
# for an existing package), and its tag still runs this job.
# A publishing run never restores a cache another run could have written.
package-manager-cache: false
- run: npm ci
# Puts the version into the element name (widget-<name>-<version>).
- run: npm run build
# No .npmrc, no NPM_TOKEN: npm detects the OIDC environment and
# authenticates against the trusted publisher of this package.
- name: npm publish
env:
PUBLISH: ${{ github.event_name == 'push' }}
run: |
name=$(node -p "require('./package.json').name")
version=$(node -p "require('./package.json').version")
if [ "$(npm view "$name@$version" version 2>/dev/null)" = "$version" ]; then
echo "$name@$version is already on npm, skipping publish"
name=$(jq -r .name package.json)
version=$(jq -r .version package.json)
if [ "$PUBLISH" != true ]; then
npm publish --access public --dry-run
elif [ "$(npm view "$name@$version" version 2>/dev/null)" = "$version" ]; then
echo "$name@$version is already on npm"
else
npm publish --access public
fi

- name: Get the new version
id: get_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.get_version.outputs.version }}" \
--title "Release ${{ steps.get_version.outputs.version }}" \
--generate-notes
# The tag once the package is on npm, with the release GitHub App's token
# from the `release` environment (deployments from main only).
tag:
needs: [npm]
if: github.event_name == 'push' && startsWith(github.event.head_commit.message, 'release ')
uses: IronFlock/workflows/.github/workflows/release-tag.yml@v1
permissions:
contents: read
# The App key is a secret of the `release` environment; environment
# secrets reach a reusable workflow only through `secrets: inherit`.
secrets: inherit # zizmor: ignore[secrets-inherit]
with:
version-file: package.json
tag-prefix: ""
# The GitHub release of the tag, with the same App token.
github-release: true
65 changes: 65 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Gate (the committed generated files are current, the widget build; no unit
# tests) and workflow lint from the shared IronFlock CI harness.
# Pull requests into main only: the ruleset lets nothing reach main without
# ci-ok. The release intent decides whether the merge releases.
name: ci

on:
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

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

jobs:
gate:
uses: IronFlock/workflows/.github/workflows/node-gate.yml@v1
with:
package-manager: npm
skip: unit

workflows:
uses: IronFlock/workflows/.github/workflows/workflows-lint.yml@v1

# The version decides: a bump makes a release PR (`just release-patch`,
# title `release widget-mapbox <version>`), an unchanged version a no-release PR
# (`just merge`) whose merge releases nothing. Tags are bare versions.
intent:
uses: IronFlock/workflows/.github/workflows/release-intent.yml@v1
with:
version-file: package.json
tag-prefix: ""

# Release pull requests only: build-publish.yml as a dry run (`npm publish
# --dry-run`, no tag), so a broken package never reaches main.
release-build:
needs: [intent]
if: needs.intent.outputs.kind == 'release'
uses: ./.github/workflows/build-publish.yml
permissions:
contents: read
id-token: write

# The required check of the main-branch ruleset: green only when every job
# above succeeded (skipped or cancelled counts as failed; release-build may be
# skipped, on a no-release pull request).
ci-ok:
if: ${{ always() }}
needs: [gate, workflows, intent, release-build]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: all ci jobs succeeded
env:
REQUIRED: ${{ needs.gate.result }} ${{ needs.workflows.result }} ${{ needs.intent.result }}
RELEASE_BUILD: ${{ needs.release-build.result }}
KIND: ${{ needs.intent.outputs.kind }}
run: |
echo "required: $REQUIRED, release-build: $RELEASE_BUILD ($KIND)"
for r in $REQUIRED; do [ "$r" = success ] || exit 1; done
if [ "$KIND" = release ]; then [ "$RELEASE_BUILD" = success ]; else [ "$RELEASE_BUILD" = skipped ]; fi
33 changes: 33 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Security screening from the shared IronFlock CI harness: secrets, npm
# dependencies (osv-scanner on package-lock.json), semgrep, configuration and
# licences.
#
# Report-only for now (fail-on empty): the report job lists every finding;
# blocking follows once they are fixed. Accepted findings go into
# .trivyignore.yaml / security/vex.openvex.json / .gitleaks.toml with a reason.
name: security

on:
pull_request:
branches: [main]
schedule:
# Mondays 12:31 UTC: catch CVEs disclosed against unchanged code.
- cron: "31 12 * * 1"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: security-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
security:
uses: IronFlock/workflows/.github/workflows/security.yml@v1
permissions:
contents: read
security-events: write
with:
languages: node
fail-on: ""
1 change: 0 additions & 1 deletion .npmrc

This file was deleted.

5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `npm run watch` - Rollup watch mode (rebuild on change).
- `npm start` - Runs `watch` and `@web/dev-server` concurrently; opens `demo/index.html` (served from repo root, see `web-dev-server.config.mjs`). The demo loads the built `dist/` bundle, and the built-in token only works on studio.ironflock.com: open `/demo/?token=<token>` with a token that allows localhost (e.g. studio's `.env.development` one) to see tiles.
- `npm run types` - Regenerate `src/definition-schema.d.ts` from `src/definition-schema.json` (`json2ts`). Run after editing the schema.
- `npm run release` — `npm version patch`: preflight guards (on `main`, clean tree, not behind `origin/main`, generated files current, build passes), then the bump and a `dist/` rebuild (the `version` hook, so the local build registers the new tag), then commit, bare-semver tag, `git push --follow-tags`, then waits on the CI run and fails if the npm publish fails. `npm run release:minor` / `release:major` for other bumps.
- `just lint` — what the CI gate checks besides the build: regenerating the committed generated files (`npm run types`) changes nothing. `just build` = `npm run build`.
- `just merge` / `just release-patch` (`release-minor`, `release-major`) — pull requests into `main` from a feature branch with everything committed: a no-release one, or a release one whose merge publishes to npm in CI (`.github/workflows/build-publish.yml`), then the release GitHub App tags the bare version and creates the GitHub release. Never `npm publish`, tag or edit the version by hand.
- `npm run link` / `npm run unlink` - Link/unlink against a sibling `../RESWARM/frontend` checkout for in-app testing.
- `npm run cors` - Updates GCS CORS for `gs://reswarm-images` (used to host the demo's `ObjectRandomizer.js`).

Expand Down Expand Up @@ -56,7 +57,7 @@ Single Rollup config (`rollup.config.js`): `replace` (version) -> `string` (CSS

### Release flow

`npm run release` bumps the patch version and pushes the tag; the empty tag prefix comes from `tag-version-prefix=""` in `.npmrc`. `.github/workflows/build-publish.yml` triggers on any tag push, runs `npm ci`, `npm run build`, then `npm publish --access public` via npm trusted publishing (OIDC — no `NPM_TOKEN`) and creates a GitHub Release. The bare numeric tag is what the workflow expects.
Release on merge, through the shared IronFlock harness (IronFlock/workflows): `main` takes pull requests only, with green `ci-ok` and `security / report`. `just release-patch` bumps `package.json` and the lockfile and opens the release pull request `release widget-mapbox <version>`; its merge runs `.github/workflows/build-publish.yml` (the name stays because npm's trusted publisher names it): `npm ci`, build, `npm publish` via npm trusted publishing (OIDC, no `NPM_TOKEN`), then the release GitHub App sets the bare-version tag (`1.0.10`, no `v`) and creates the GitHub Release. `just merge` opens a pull request that releases nothing.

## `aiSelection` in `src/definition-schema.json`

Expand Down
70 changes: 70 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# The recipes the CI gate runs and the release, the same in every widget
# repository (IronFlock/widget-*) but for RELEASE_NAME.

# The package name without its scope; the tags are bare versions (1.0.10).
RELEASE_NAME := "widget-mapbox"
VERSION_FILE := "package.json"
# Every file _write-version changes; the release commit holds exactly these.
VERSION_FILES := "package.json package-lock.json"

# Generated files are committed (`npm run types`, and `catalog` where it
# exists): regenerating them must change nothing, also next to uncommitted
# work.
#
# Lint as CI runs it: the generated files are current
lint:
#!/usr/bin/env bash
set -euo pipefail
before=$(git diff | git hash-object --stdin)
for gen in types catalog; do
if [ -n "$(jq -r --arg g "$gen" '.scripts[$g] // empty' package.json)" ]; then
npm run --silent "$gen"
fi
done
if [ "$(git diff | git hash-object --stdin)" != "$before" ]; then
echo "generated files were stale (now regenerated): commit them" >&2
git diff --stat >&2
exit 1
fi

# Build the widget bundle (dist/)
build:
npm run build

# ------------------------------------------------------------------ release --
# Pull requests into main, from IronFlock/workflows (scripts/pr.sh at v1); see
# its README, "Pull requests from a laptop":
# just merge [TITLE] a no-release pull request from this feature branch
# just release-patch a release pull request (-minor, -major): the bump
# from main's version on top of this branch (or on a
# new branch release/<x> from an up-to-date main)
# Both merge automatically once ci and security are green. The merge of a
# release pull request runs .github/workflows/build-publish.yml: npm publish,
# then the tag <version> (release GitHub App), then the GitHub release.

# Writes the version; called by pr.sh with the next version.
_write-version version:
npm version "{{version}}" --no-git-tag-version --allow-same-version

[positional-arguments]
_pr *args:
#!/usr/bin/env bash
set -euo pipefail
script=$(mktemp)
trap 'rm -f "$script"' EXIT
gh api -H 'Accept: application/vnd.github.raw' \
'repos/IronFlock/workflows/contents/scripts/pr.sh?ref=v1' > "$script"
bash "$script" "$@"

[positional-arguments]
merge TITLE="":
just _pr merge {{VERSION_FILE}} "$1"

release-patch:
TAG_PREFIX= just _pr release patch {{RELEASE_NAME}} {{VERSION_FILE}} build-publish.yml {{VERSION_FILES}}

release-minor:
TAG_PREFIX= just _pr release minor {{RELEASE_NAME}} {{VERSION_FILE}} build-publish.yml {{VERSION_FILES}}

release-major:
TAG_PREFIX= just _pr release major {{RELEASE_NAME}} {{VERSION_FILE}} build-publish.yml {{VERSION_FILES}}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Give the element a size (it fills its height, which must be definite), and set i
npm start # rollup watch + web-dev-server, opens demo/index.html
npm run build # dist/widget-mapbox.js
npm run types # regenerate src/definition-schema.d.ts after editing the schema
npm run release # patch release (see CLAUDE.md)
just release-patch # release pull request (see CLAUDE.md)
```

The demo loads the built bundle from `dist/`. Map tiles need a token that allows localhost: open `/demo/?token=<token>`.
8 changes: 1 addition & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,7 @@
"link": "npm link && cd ../RESWARM/frontend && npm link @ironflock/widget-mapbox",
"unlink": "npm unlink --global && cd ../RESWARM/frontend && npm unlink @ironflock/widget-mapbox && npm i @ironflock/widget-mapbox",
"types": "cat src/definition-schema.json | json2ts > src/definition-schema.d.ts",
"cors": "gcloud storage buckets update gs://reswarm-images --cors-file=src/cors-config.json",
"preversion": "sh scripts/release-preflight.sh",
"version": "npm run build",
"postversion": "git push --follow-tags && sh scripts/release-verify.sh",
"release": "npm version patch",
"release:minor": "npm version minor",
"release:major": "npm version major"
"cors": "gcloud storage buckets update gs://reswarm-images --cors-file=src/cors-config.json"
},
"dependencies": {
"lit": "^3.3.3"
Expand Down
39 changes: 0 additions & 39 deletions scripts/release-preflight.sh

This file was deleted.

Loading
Loading