@moku-labs/ci 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ # Thin caller for a Layer-3 moku app: validate on every PR, deploy to Cloudflare on push to main.
4
+ on:
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+
9
+ # Cancel superseded runs on the same ref. Safe here — app-deploy.yml sets no group of its own.
10
+ concurrency:
11
+ group: ci-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ ci:
19
+ uses: moku-labs/ci/.github/workflows/app-deploy.yml@v1
20
+ with:
21
+ test_script: test:coverage
22
+ build_worker_script: build:worker # client build first — the worker dry-run reads dist/client
23
+ migrate_script: migrate:remote # wrangler d1 migrations apply <db> --remote
24
+ deploy_script: deploy # wrangler deploy
25
+ # Reusable workflows do NOT inherit secrets — pass each one explicitly.
26
+ secrets:
27
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
28
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -0,0 +1,22 @@
1
+ name: CI
2
+
3
+ # Thin caller. All logic lives in moku-labs/ci, pinned to the moving @v1 major tag.
4
+ # Required status checks land as "ci / lint", "ci / types", "ci / test", "ci / build"
5
+ # (GitHub prefixes a reusable workflow's jobs with the CALLER's job id) — see rulesets/main.json.
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ pull_request:
10
+ # No `workflow_call:` needed — publish.yml reaches the same checks through
11
+ # package-release.yml, which calls package-ci.yml itself.
12
+
13
+ # NO concurrency block here on purpose: package-ci.yml already scopes its group by
14
+ # github.workflow (= this workflow's name), and a caller group with the same value would
15
+ # deadlock against its own child.
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ ci:
22
+ uses: moku-labs/ci/.github/workflows/package-ci.yml@v1
@@ -0,0 +1,84 @@
1
+ name: Release
2
+
3
+ # FALLBACK variant of publish.yml — use it if npm rejects a publish executed inside a
4
+ # reusable workflow owned by another repo (see README "Known risk"). Copy this over
5
+ # publish.yml (keep the filename — it is the Trusted Publishing contract).
6
+ #
7
+ # The central workflow still owns check → release → package. Only the credentialed
8
+ # `npm publish` runs HERE, in this repo's own publish.yml, so the OIDC claim's
9
+ # job_workflow_ref points at <owner>/<repo>/.github/workflows/publish.yml.
10
+ on:
11
+ workflow_dispatch:
12
+ inputs:
13
+ release_type:
14
+ description: Semver bump for this release
15
+ type: choice
16
+ required: true
17
+ options: [patch, minor, major, prerelease]
18
+ release:
19
+ types: [published]
20
+
21
+ concurrency:
22
+ group: publish-${{ github.ref }}
23
+ cancel-in-progress: false
24
+
25
+ permissions:
26
+ contents: read
27
+
28
+ jobs:
29
+ release:
30
+ uses: moku-labs/ci/.github/workflows/package-release.yml@v1
31
+ permissions:
32
+ contents: write # release job only; no id-token — nothing central publishes now
33
+ with:
34
+ release_type: ${{ inputs.release_type }}
35
+ publish: false # the central publish job is skipped; the local one below takes over
36
+
37
+ publish:
38
+ needs: release
39
+ if: |
40
+ always() && needs.release.outputs.artifact_name != '' &&
41
+ (
42
+ (github.event_name == 'workflow_dispatch' && needs.release.outputs.tag != '') ||
43
+ github.event_name == 'release'
44
+ )
45
+ runs-on: ubuntu-latest
46
+ permissions:
47
+ id-token: write # ONLY this job — tokenless Trusted Publishing via OIDC
48
+ contents: read
49
+ steps:
50
+ - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
51
+ with:
52
+ ref: ${{ github.event_name == 'workflow_dispatch' && needs.release.outputs.tag || github.ref }}
53
+ persist-credentials: false
54
+ - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
55
+ with:
56
+ node-version: 24
57
+ registry-url: https://registry.npmjs.org
58
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
59
+ with:
60
+ name: ${{ needs.release.outputs.artifact_name }}
61
+ path: dist-pack
62
+ - name: Assert npm floor for Trusted Publishing (fail closed)
63
+ run: |
64
+ set -euo pipefail
65
+ # Rely on the npm bundled with Node 24 — never `npm install -g npm@latest` next to
66
+ # a publish credential. Assert the floor instead.
67
+ have="$(npm --version)"; need="11.5.1"
68
+ [ "$(printf '%s\n%s\n' "$need" "$have" | sort -V | head -1)" = "$need" ] \
69
+ || { echo "npm $have < $need (Trusted Publishing floor)"; exit 1; }
70
+ - name: Verify the ref matches package.json, then publish
71
+ env:
72
+ EVENT: ${{ github.event_name }}
73
+ DISPATCH_VERSION: ${{ needs.release.outputs.version }}
74
+ REF_NAME: ${{ github.ref_name }}
75
+ run: |
76
+ set -euo pipefail
77
+ pkg="$(node -p "require('./package.json').version")"
78
+ if [ "$EVENT" = "workflow_dispatch" ]; then want="$DISPATCH_VERSION"; else want="${REF_NAME#v}"; fi
79
+ [ -n "$want" ] || { echo "empty version/ref — refusing to publish"; exit 1; }
80
+ [ "$pkg" = "$want" ] || { echo "package.json $pkg != ref $want"; exit 1; }
81
+ # Prereleases go to dist-tag 'next' so they never clobber 'latest'.
82
+ case "$pkg" in *-*) tag=next ;; *) tag=latest ;; esac
83
+ # Leading "./" is REQUIRED: a bare path is parsed by npm as an "owner/repo" spec.
84
+ npm publish ./dist-pack/*.tgz --tag "$tag" --access public
@@ -0,0 +1,36 @@
1
+ name: Release
2
+
3
+ # Thin caller. The FILENAME is a contract: npm Trusted Publishing is registered against
4
+ # "publish.yml" for this repo — renaming it breaks tokenless publishing.
5
+ on:
6
+ workflow_dispatch:
7
+ inputs:
8
+ release_type:
9
+ description: Semver bump for this release
10
+ type: choice
11
+ required: true
12
+ options: [patch, minor, major, prerelease]
13
+ release:
14
+ types: [published]
15
+
16
+ # Distinct literal group ("publish-…") — NOT github.workflow. The reused package-ci.yml
17
+ # computes its group from the CALLER's github.workflow = "Release", i.e. "Release-<ref>".
18
+ # If this run also used "Release-<ref>" it would hold that slot for its whole duration while
19
+ # its own child waits for the same slot → deadlock, and the reusable workflow never starts.
20
+ concurrency:
21
+ group: publish-${{ github.ref }}
22
+ cancel-in-progress: false
23
+
24
+ permissions:
25
+ contents: read # least privilege at the top; the release job elevates below
26
+
27
+ jobs:
28
+ release:
29
+ uses: moku-labs/ci/.github/workflows/package-release.yml@v1
30
+ # A called workflow can NEVER elevate beyond what the caller grants. These two are the
31
+ # ceiling for the whole pipeline; inside, each job takes only the one it needs.
32
+ permissions:
33
+ contents: write # release job: push refs/tags/* + create the GitHub release
34
+ id-token: write # publish job: tokenless OIDC Trusted Publishing
35
+ with:
36
+ release_type: ${{ inputs.release_type }}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@moku-labs/ci",
3
+ "version": "1.0.0",
4
+ "description": "Central CI and release for the moku family: reusable workflows, caller examples and the moku-release CLI.",
5
+ "type": "module",
6
+ "sideEffects": ["./dist/release.mjs", "./src/index.ts"],
7
+ "bin": {
8
+ "moku-release": "./dist/release.mjs"
9
+ },
10
+ "files": ["dist", "examples", "rulesets", "LICENSE", "README.md"],
11
+ "engines": {
12
+ "node": ">=24.0.0",
13
+ "bun": ">=1.3.14"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/moku-labs/ci.git"
18
+ },
19
+ "license": "MIT",
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "registry": "https://registry.npmjs.org/"
23
+ },
24
+ "devDependencies": {
25
+ "@biomejs/biome": "2.4.16",
26
+ "@moku-labs/common": "0.3.0",
27
+ "@types/bun": "1.3.14",
28
+ "@vitest/coverage-istanbul": "4.0.18",
29
+ "eslint": "9.39.3",
30
+ "eslint-config-biome": "2.1.3",
31
+ "eslint-plugin-jsdoc": "62.6.0",
32
+ "eslint-plugin-sonarjs": "4.0.0",
33
+ "eslint-plugin-unicorn": "63.0.0",
34
+ "globals": "17.4.0",
35
+ "jiti": "2.6.1",
36
+ "publint": "0.3.21",
37
+ "tsdown": "0.22.1",
38
+ "typescript": "6.0.3",
39
+ "typescript-eslint": "8.58.0",
40
+ "vitest": "4.0.18"
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "validate": "publint",
45
+ "typecheck": "tsc --noEmit",
46
+ "lint": "biome check . && eslint .",
47
+ "lint:fix": "biome check --write . && eslint --fix .",
48
+ "format": "biome format --write .",
49
+ "test": "vitest run",
50
+ "release:setup": "moku-release setup",
51
+ "release:doctor": "moku-release doctor",
52
+ "release": "moku-release"
53
+ }
54
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "protect-main",
3
+ "target": "branch",
4
+ "enforcement": "active",
5
+ "bypass_actors": [],
6
+ "conditions": { "ref_name": { "include": ["refs/heads/main"], "exclude": [] } },
7
+ "rules": [
8
+ { "type": "deletion" },
9
+ { "type": "non_fast_forward" },
10
+ {
11
+ "type": "pull_request",
12
+ "parameters": {
13
+ "required_approving_review_count": 0,
14
+ "dismiss_stale_reviews_on_push": true,
15
+ "require_code_owner_review": false,
16
+ "require_last_push_approval": false,
17
+ "required_review_thread_resolution": false
18
+ }
19
+ },
20
+ {
21
+ "type": "required_status_checks",
22
+ "parameters": {
23
+ "strict_required_status_checks_policy": true,
24
+ "required_status_checks": [
25
+ { "context": "ci / lint" },
26
+ { "context": "ci / types" },
27
+ { "context": "ci / test" },
28
+ { "context": "ci / build" }
29
+ ]
30
+ }
31
+ }
32
+ ]
33
+ }