create-daloy 0.34.2 → 0.35.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.
- package/README.md +62 -6
- package/bin/create-daloy.mjs +344 -25
- package/package.json +2 -2
- package/sbom.cdx.json +13 -9
- package/sbom.spdx.json +5 -5
- package/templates/_ci/deno/SECURITY.md +31 -2
- package/templates/_ci/deno/_github/CODEOWNERS +2 -0
- package/templates/_ci/deno/_github/workflows/container-scan.yml +42 -8
- package/templates/_ci/deno/_github/workflows/deploy.yml +183 -0
- package/templates/_ci/deno/_github/workflows/opengrep.yml +137 -0
- package/templates/_ci/deno/_github/workflows/osv-scan.yml +121 -0
- package/templates/_ci/deno/_github/workflows/secret-scan.yml +106 -0
- package/templates/_ci/node/SECURITY.md +100 -1
- package/templates/_ci/node/_github/CODEOWNERS +3 -0
- package/templates/_ci/node/_github/workflows/container-scan.yml +46 -10
- package/templates/_ci/node/_github/workflows/dast.yml +177 -0
- package/templates/_ci/node/_github/workflows/deploy.yml +94 -0
- package/templates/_ci/node/_github/workflows/opengrep.yml +169 -0
- package/templates/_ci/node/_github/workflows/osv-scan.yml +135 -0
- package/templates/_ci/node/_github/workflows/secret-scan.yml +106 -0
- package/templates/bun-basic/AGENTS.md +12 -0
- package/templates/bun-basic/_gitignore +5 -0
- package/templates/bun-basic/package.json +3 -2
- package/templates/cloudflare-worker/AGENTS.md +13 -0
- package/templates/cloudflare-worker/_gitignore +9 -0
- package/templates/cloudflare-worker/_npmrc +2 -1
- package/templates/cloudflare-worker/package.json +3 -2
- package/templates/deno-basic/AGENTS.md +12 -0
- package/templates/deno-basic/_gitignore +5 -0
- package/templates/deno-basic/deno.json +2 -2
- package/templates/node-basic/AGENTS.md +12 -0
- package/templates/node-basic/_gitignore +5 -0
- package/templates/node-basic/_npmrc +2 -2
- package/templates/node-basic/package.json +1 -1
- package/templates/vercel-edge/AGENTS.md +12 -0
- package/templates/vercel-edge/_gitignore +5 -0
- package/templates/vercel-edge/_npmrc +2 -1
- package/templates/vercel-edge/package.json +3 -2
package/README.md
CHANGED
|
@@ -53,10 +53,11 @@ pnpm create daloy@latest my-api \
|
|
|
53
53
|
| `--template <name>` | `node-basic` (default), `vercel-edge`, `cloudflare-worker`, `bun-basic`, or `deno-basic`. |
|
|
54
54
|
| `--package-manager <pm>` | `pnpm` (default), `npm`, `yarn`, or `bun`. Ignored for `deno-basic`. |
|
|
55
55
|
| `--list-templates` | Print available templates with descriptions. |
|
|
56
|
-
| `--install` / `--no-install` | Install dependencies after scaffolding. Defaults to **Y** for npm/yarn/bun and **N** for pnpm (so first-time runs are not blocked by the 24h `minimumReleaseAge` embargo
|
|
56
|
+
| `--install` / `--no-install` | Install dependencies after scaffolding. Defaults to **Y** for npm/yarn/bun and **N** for pnpm (so first-time runs are not blocked by the 24h `minimumReleaseAge` embargo and so you can review the scaffold's hardened `.npmrc` and `pnpm-workspace.yaml` before the first install). |
|
|
57
57
|
| `--git` / `--no-git` | Initialize a git repository. Defaults to interactive. |
|
|
58
58
|
| `--minimal` | Strip the bookstore demo route and the built-in `/docs` + `/openapi.json` routes so only the framework bootstrap and `/healthz` ship. |
|
|
59
59
|
| `--with-ci` / `--no-ci` | Add the hardened GitHub Actions, Dependabot, CODEOWNERS, SECURITY.md, and lockfile-source verification bundle. **Defaults to Y** so scaffolded projects are secure by default. |
|
|
60
|
+
| `--with-deploy` / `--no-deploy` | Add the starter `.github/workflows/deploy.yml`. Defaults to the same value as `--with-ci`, so you can keep CI but opt out of deploy scaffolding with `--no-deploy`. |
|
|
60
61
|
| `--code-owner <owner>` | Replace the CODEOWNERS placeholder when `--with-ci` is used, for example `@acme/security`. |
|
|
61
62
|
| `--force` | Overwrite an existing non-empty directory. |
|
|
62
63
|
| `--yes` | Accept all defaults; never prompt. |
|
|
@@ -85,8 +86,7 @@ A minimal Cloudflare Worker bootstrap using `@daloyjs/core/cloudflare` with:
|
|
|
85
86
|
|
|
86
87
|
- `wrangler.toml` ready to deploy.
|
|
87
88
|
- `secureHeaders` and `requestId` enabled by default, with smaller edge-friendly body and timeout limits.
|
|
88
|
-
- Zod-validated route exposed
|
|
89
|
-
- A sample test that exercises `app.request(...)`.
|
|
89
|
+
- A Zod-validated `/healthz` route and contract-first `/books/:id` route exposed via `toFetchHandler(app)`.
|
|
90
90
|
|
|
91
91
|
### `vercel-edge`
|
|
92
92
|
|
|
@@ -153,11 +153,51 @@ For Node-style templates, the bundle adds:
|
|
|
153
153
|
- `.github/workflows/ci.yml` with top-level `permissions: {}`, pinned actions,
|
|
154
154
|
`harden-runner`, `persist-credentials: false`, no package-manager cache, and
|
|
155
155
|
install scripts disabled.
|
|
156
|
+
- `.github/workflows/deploy.yml` as a manual-only deployment starter. Container
|
|
157
|
+
templates publish a Docker image to GHCR with the repo-scoped `GITHUB_TOKEN`,
|
|
158
|
+
while Vercel and Cloudflare templates ship concrete CLI deploy steps that
|
|
159
|
+
read their platform credentials from GitHub Actions secrets/variables. The
|
|
160
|
+
deploy job is gated to `main` or a tag by default, and Node-style templates
|
|
161
|
+
re-run `verify:lockfile` before shipping.
|
|
156
162
|
- `.github/workflows/vuln-scan.yml` — a daily scheduled SCA cron that runs the
|
|
157
163
|
package manager's audit against the committed lockfile. Catches CVEs disclosed
|
|
158
164
|
*after* the last PR or push and provides SOC 2 CC7.1
|
|
159
165
|
([continuous vulnerability management](https://www.aikido.dev/blog/a-guide-to-automating-technical-vulnerability-management-for-soc-2))
|
|
160
166
|
evidence even when developers are not touching the repo.
|
|
167
|
+
- `.github/workflows/osv-scan.yml` — a SECOND, independent SCA source.
|
|
168
|
+
`vuln-scan.yml` queries the package manager's audit feed (GHSA); this one
|
|
169
|
+
runs Google's OSV-Scanner against the committed lockfile and cross-references
|
|
170
|
+
the OpenSSF
|
|
171
|
+
[malicious-packages](https://github.com/ossf/malicious-packages) corpus, so
|
|
172
|
+
a malware advisory that lands in OSV.dev before it propagates to GHSA still
|
|
173
|
+
fails the build. The binary is downloaded from a pinned official release and
|
|
174
|
+
verified by SHA-256 before execution — no third-party action is added to the
|
|
175
|
+
supply chain just for this scan. This is the missing layer the Aikido
|
|
176
|
+
[SAST vs SCA](https://www.aikido.dev/blog/sast-vs-sca) and
|
|
177
|
+
[npm-audit-guide](https://www.aikido.dev/blog/npm-audit-guide) write-ups
|
|
178
|
+
warn about, and the Deno scaffold gets it too (Deno has no `audit` built
|
|
179
|
+
in, so without OSV-Scanner a Deno scaffold would have no scheduled SCA at
|
|
180
|
+
all).
|
|
181
|
+
- `.github/workflows/secret-scan.yml` — runs [gitleaks](https://github.com/gitleaks/gitleaks)
|
|
182
|
+
on every PR / push (working tree) and on a daily schedule across the **full
|
|
183
|
+
git history**, so a credential leaked anywhere in any commit, branch, or tag
|
|
184
|
+
is surfaced even if GitHub-native push protection missed it. The gitleaks
|
|
185
|
+
binary is downloaded from a pinned official release and verified by SHA-256
|
|
186
|
+
before execution — no third-party action is added to the supply chain just
|
|
187
|
+
for this scan. See Aikido's
|
|
188
|
+
[Secrets Detection guide](https://www.aikido.dev/blog/secret-detection-application-security)
|
|
189
|
+
for why history-aware scanning is the floor and not the ceiling.
|
|
190
|
+
- `.github/workflows/opengrep.yml` — a second SAST source alongside CodeQL,
|
|
191
|
+
using [Opengrep](https://github.com/opengrep/opengrep) (an open-source
|
|
192
|
+
Semgrep fork) with the same pinned-binary + SHA-256-verified pattern as the
|
|
193
|
+
OSV and gitleaks scans.
|
|
194
|
+
- `.github/workflows/container-scan.yml` — runs Trivy against the image
|
|
195
|
+
produced by the template's `_Dockerfile` (filesystem scan on PR, full image
|
|
196
|
+
scan on push to `main`) so a base-image CVE or a vulnerable layer is
|
|
197
|
+
surfaced before deploy.
|
|
198
|
+
- `.github/workflows/dast.yml` — a manual-only dynamic-analysis workflow that
|
|
199
|
+
boots the scaffolded API and runs an OWASP ZAP baseline scan against it,
|
|
200
|
+
for teams that want a black-box check before promoting a release.
|
|
161
201
|
- CodeQL, OpenSSF Scorecard, zizmor, Dependabot, CODEOWNERS, and `SECURITY.md`.
|
|
162
202
|
- `scripts/verify-lockfile-sources.mjs` plus a `verify:lockfile` package script
|
|
163
203
|
that rejects git dependencies and non-registry tarball URLs in text lockfiles.
|
|
@@ -166,20 +206,36 @@ The bundle deliberately does **not** generate an npm publish workflow.
|
|
|
166
206
|
`create-daloy` scaffolds REST API services, not libraries; if you later carve
|
|
167
207
|
out a reusable package, opt into npm trusted publishing yourself.
|
|
168
208
|
|
|
169
|
-
For `deno-basic`, `--with-ci` generates a Deno-native CI workflow
|
|
170
|
-
|
|
209
|
+
For `deno-basic`, `--with-ci` generates a Deno-native CI workflow, a manual-only
|
|
210
|
+
container publish starter for GHCR that is guarded to `main` or a tag by
|
|
211
|
+
default, plus CodeQL, Opengrep, **OSV-Scanner** (the only scheduled SCA layer
|
|
212
|
+
a Deno scaffold has, since Deno ships no `audit`), Scorecard, zizmor,
|
|
213
|
+
Dependabot for GitHub Actions, CODEOWNERS, and `SECURITY.md`.
|
|
214
|
+
|
|
215
|
+
If you want the governance bundle but not the deployment starter, pass
|
|
216
|
+
`--with-ci --no-deploy`. If you only want a deployment starter, pass
|
|
217
|
+
`--with-deploy --no-ci`.
|
|
171
218
|
|
|
172
219
|
If you omit `--code-owner`, the generated CODEOWNERS file uses
|
|
173
220
|
`@your-org/security-team` as a placeholder. Replace it before relying on branch
|
|
174
221
|
protection. You should also enable GitHub secret scanning, push protection, and
|
|
175
222
|
required status checks in the repository settings.
|
|
176
223
|
|
|
224
|
+
## Container-first scaffolds
|
|
225
|
+
|
|
226
|
+
Every template (Node, Bun, Vercel Edge, Cloudflare Worker, and Deno) ships a
|
|
227
|
+
production-oriented `Dockerfile` and `.dockerignore` with the secure-by-default
|
|
228
|
+
posture from `@daloyjs/core` `0.24.0`: a non-root user, `STOPSIGNAL SIGTERM`,
|
|
229
|
+
`tini` as PID 1, and a `HEALTHCHECK` pointed at `/readyz`. Node-style templates
|
|
230
|
+
also ship an `.env.example`. None of this is required — delete or replace
|
|
231
|
+
whatever you do not need.
|
|
232
|
+
|
|
177
233
|
## What the CLI guarantees
|
|
178
234
|
|
|
179
235
|
- Zero runtime dependencies (uses only Node built-ins) for a clean supply-chain footprint.
|
|
180
236
|
- A modern terminal experience with Unicode/color capability detection and ASCII fallbacks.
|
|
181
237
|
- Templates are copied verbatim from this package's `templates/` directory.
|
|
182
|
-
- Files and folders prefixed with `_` are renamed on copy (`_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`, `_github/` → `.github`, `_agents/` → `.agents
|
|
238
|
+
- Files and folders prefixed with `_` are renamed on copy (`_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`, `_github/` → `.github`, `_agents/` → `.agents/`, `_Dockerfile` → `Dockerfile`, `_dockerignore` → `.dockerignore`, `_env.example` → `.env.example`) to survive npm packing.
|
|
183
239
|
- pnpm-specific `.npmrc` hardening is kept only when you choose `pnpm`; other package managers get a clean project without unsupported config warnings.
|
|
184
240
|
- pnpm projects ship with `ignore-scripts=true`, `minimum-release-age=1440`, `verify-store-integrity=true`, `prefer-frozen-lockfile=true`, and `strict-peer-dependencies=true` by default.
|
|
185
241
|
- `--with-ci` projects ship with pinned GitHub Actions workflows, CODEOWNERS, Dependabot, SECURITY.md, and lockfile-source verification.
|
package/bin/create-daloy.mjs
CHANGED
|
@@ -329,10 +329,11 @@ ${heading("Options")}
|
|
|
329
329
|
${color(COLORS.green, "--template <name>")} ${TEMPLATES.join(" | ")} ${color(COLORS.dim, "(default: node-basic)")}
|
|
330
330
|
${color(COLORS.green, "--package-manager <pm>")} ${PACKAGE_MANAGERS.join(" | ")} ${color(COLORS.dim, "(default: pnpm)")}
|
|
331
331
|
${color(COLORS.green, "--list-templates")} Print available templates and exit.
|
|
332
|
-
${color(COLORS.green, "--install / --no-install")} Install dependencies after scaffolding. ${color(COLORS.dim, "(default: Y, except pnpm \u2014 N to respect minimumReleaseAge +
|
|
332
|
+
${color(COLORS.green, "--install / --no-install")} Install dependencies after scaffolding. ${color(COLORS.dim, "(default: Y, except pnpm \u2014 N to respect minimumReleaseAge + ignore-scripts review)")}
|
|
333
333
|
${color(COLORS.green, "--git / --no-git")} Initialize a git repository.
|
|
334
334
|
${color(COLORS.green, "--minimal")} Strip the bookstore + OpenAPI docs demo routes.
|
|
335
335
|
${color(COLORS.green, "--with-ci / --no-ci")} Add hardened GitHub Actions + governance files. ${color(COLORS.dim, "(default: Y)")}
|
|
336
|
+
${color(COLORS.green, "--with-deploy / --no-deploy")} Add starter deploy.yml workflow(s). ${color(COLORS.dim, "(default: inherits --with-ci)")}
|
|
336
337
|
${color(COLORS.green, "--code-owner <owner>")} CODEOWNERS owner for --with-ci, e.g. @acme/security.
|
|
337
338
|
${color(COLORS.green, "--force")} Overwrite an existing non-empty directory.
|
|
338
339
|
${color(COLORS.green, "--yes, -y")} Accept all defaults; never prompt.
|
|
@@ -380,6 +381,7 @@ function parseArgs(argv) {
|
|
|
380
381
|
listTemplates: false,
|
|
381
382
|
minimal: false,
|
|
382
383
|
ci: undefined,
|
|
384
|
+
deploy: undefined,
|
|
383
385
|
codeOwner: undefined,
|
|
384
386
|
};
|
|
385
387
|
const args = [...argv];
|
|
@@ -393,6 +395,8 @@ function parseArgs(argv) {
|
|
|
393
395
|
else if (a === "--minimal") out.minimal = true;
|
|
394
396
|
else if (a === "--with-ci") out.ci = true;
|
|
395
397
|
else if (a === "--no-ci") out.ci = false;
|
|
398
|
+
else if (a === "--with-deploy") out.deploy = true;
|
|
399
|
+
else if (a === "--no-deploy") out.deploy = false;
|
|
396
400
|
else if (a === "--code-owner") out.codeOwner = args.shift();
|
|
397
401
|
else if (a?.startsWith("--code-owner=")) out.codeOwner = a.slice("--code-owner=".length);
|
|
398
402
|
else if (a === "--install") out.install = true;
|
|
@@ -686,6 +690,15 @@ function runScriptCommand(packageManager, scriptName) {
|
|
|
686
690
|
return `${packageManager} run ${scriptName}`;
|
|
687
691
|
}
|
|
688
692
|
|
|
693
|
+
function execCommand(packageManager, binary, args = "") {
|
|
694
|
+
const suffix = args ? ` ${args}` : "";
|
|
695
|
+
if (packageManager === "pnpm") return `pnpm exec ${binary}${suffix}`;
|
|
696
|
+
if (packageManager === "npm") return `npx ${binary}${suffix}`;
|
|
697
|
+
if (packageManager === "yarn") return `yarn exec ${binary}${suffix}`;
|
|
698
|
+
if (packageManager === "bun") return `bunx ${binary}${suffix}`;
|
|
699
|
+
return `${binary}${suffix}`;
|
|
700
|
+
}
|
|
701
|
+
|
|
689
702
|
function installCommand(packageManager) {
|
|
690
703
|
if (packageManager === "pnpm") return "pnpm install --frozen-lockfile --ignore-scripts";
|
|
691
704
|
if (packageManager === "npm") return "npm ci --ignore-scripts";
|
|
@@ -740,6 +753,227 @@ function setupBunStep() {
|
|
|
740
753
|
bun-version: latest`;
|
|
741
754
|
}
|
|
742
755
|
|
|
756
|
+
function setupNodeStep() {
|
|
757
|
+
return ` - name: Set up Node.js
|
|
758
|
+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
759
|
+
with:
|
|
760
|
+
node-version: 24
|
|
761
|
+
# Package-manager caching is intentionally disabled. Shared caches
|
|
762
|
+
# can bridge fork PRs into trusted branches when mis-keyed.`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function installDependenciesStep(command) {
|
|
766
|
+
return ` - name: Install dependencies
|
|
767
|
+
run: ${command}
|
|
768
|
+
env:
|
|
769
|
+
npm_config_ignore_scripts: "true"`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function joinWorkflowBlocks(blocks) {
|
|
773
|
+
return blocks.filter(Boolean).join("\n\n");
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Deploy job ref guard. `workflow_dispatch` accepts any branch by default, so
|
|
777
|
+
// a junior could trigger a production deploy from an unreviewed feature
|
|
778
|
+
// branch by accident. This `if:` limits the deploy job to `main` or a tag
|
|
779
|
+
// push while still allowing maintainers to flip the guard off if they need
|
|
780
|
+
// to deploy from a release branch.
|
|
781
|
+
function deployRefGuard() {
|
|
782
|
+
return ` # Lock production deploys to main or a tag push. \`workflow_dispatch\`
|
|
783
|
+
# accepts any branch, so this guard stops an accidental dispatch from a
|
|
784
|
+
# feature branch from shipping unreviewed code.
|
|
785
|
+
if: github.ref == 'refs/heads/main' || github.ref_type == 'tag'`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function containerRegistryHeader() {
|
|
789
|
+
return `# Default target: GitHub Container Registry (GHCR).
|
|
790
|
+
# No extra secret is required; the workflow uses the repo-scoped GITHUB_TOKEN.
|
|
791
|
+
# After the image is published, point your platform at GHCR or replace the
|
|
792
|
+
# final push steps with your provider's deploy command.
|
|
793
|
+
#
|
|
794
|
+
# Every pushed image is also signed and attested with Sigstore Cosign
|
|
795
|
+
# (keyless / OIDC) and an SPDX SBOM, so consumers can verify provenance
|
|
796
|
+
# with \`cosign verify\` and \`cosign verify-attestation --type spdxjson\`
|
|
797
|
+
# instead of trusting the registry alone. See Aikido's "Container
|
|
798
|
+
# Security Best Practices" (https://www.aikido.dev/blog/container-security-best-practices)
|
|
799
|
+
# for why signed images + SBOM attestation are the supply-chain floor.
|
|
800
|
+
#
|
|
801
|
+
# Optional container-host handoff examples:
|
|
802
|
+
# - Fly.io: install flyctl in a later step and run
|
|
803
|
+
# flyctl deploy --image "$IMAGE_REPO@\${IMAGE_DIGEST}" --remote-only
|
|
804
|
+
# - Render: create an image-backed service that tracks
|
|
805
|
+
# ghcr.io/<owner>/<repo>@\${IMAGE_DIGEST}`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function containerPublishSteps() {
|
|
809
|
+
return ` - name: Log in to GHCR
|
|
810
|
+
env:
|
|
811
|
+
GH_TOKEN: \${{ github.token }}
|
|
812
|
+
run: |
|
|
813
|
+
set -eu
|
|
814
|
+
echo "$GH_TOKEN" | docker login ghcr.io -u "\${{ github.actor }}" --password-stdin
|
|
815
|
+
|
|
816
|
+
- name: Derive image name
|
|
817
|
+
run: |
|
|
818
|
+
set -eu
|
|
819
|
+
owner="\${GITHUB_REPOSITORY_OWNER,,}"
|
|
820
|
+
repo="\${GITHUB_REPOSITORY#*/}"
|
|
821
|
+
repo="\${repo,,}"
|
|
822
|
+
echo "IMAGE_REPO=ghcr.io/\${owner}/\${repo}" >> "$GITHUB_ENV"
|
|
823
|
+
|
|
824
|
+
- name: Build image
|
|
825
|
+
env:
|
|
826
|
+
DOCKER_BUILDKIT: "1"
|
|
827
|
+
run: |
|
|
828
|
+
set -eu
|
|
829
|
+
docker build \
|
|
830
|
+
--tag "$IMAGE_REPO:sha-\${GITHUB_SHA}" \
|
|
831
|
+
--tag "$IMAGE_REPO:latest" \
|
|
832
|
+
.
|
|
833
|
+
|
|
834
|
+
- name: Push image
|
|
835
|
+
run: |
|
|
836
|
+
set -eu
|
|
837
|
+
docker push "$IMAGE_REPO:sha-\${GITHUB_SHA}"
|
|
838
|
+
docker push "$IMAGE_REPO:latest"
|
|
839
|
+
|
|
840
|
+
- name: Resolve pushed image digest
|
|
841
|
+
# Resolve and pin the immutable digest the rest of the workflow
|
|
842
|
+
# signs and attests against. Signing a mutable tag would let any
|
|
843
|
+
# later push silently re-point a "verified" tag at attacker
|
|
844
|
+
# content; binding cosign + the SBOM attestation to
|
|
845
|
+
# \${IMAGE_REPO}@sha256:<digest> closes that race.
|
|
846
|
+
run: |
|
|
847
|
+
set -eu
|
|
848
|
+
digest="$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE_REPO:sha-\${GITHUB_SHA}" | awk -F@ '{print $2}')"
|
|
849
|
+
if [ -z "$digest" ]; then
|
|
850
|
+
echo "::error::Failed to resolve image digest for $IMAGE_REPO:sha-\${GITHUB_SHA}" >&2
|
|
851
|
+
exit 1
|
|
852
|
+
fi
|
|
853
|
+
echo "IMAGE_DIGEST=$digest" >> "$GITHUB_ENV"
|
|
854
|
+
echo "IMAGE_REF=$IMAGE_REPO@$digest" >> "$GITHUB_ENV"
|
|
855
|
+
echo "::notice::Signed image reference: $IMAGE_REPO@$digest"
|
|
856
|
+
|
|
857
|
+
- name: Install Cosign
|
|
858
|
+
# SHA-pinned per the project's actions-pinning policy. v4.1.2,
|
|
859
|
+
# commit 6f9f17788090df1f26f669e9d70d6ae9567deba6.
|
|
860
|
+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
|
861
|
+
|
|
862
|
+
- name: Sign image with Cosign (keyless / OIDC)
|
|
863
|
+
# Keyless signing uses the workflow's \`id-token\` OIDC identity
|
|
864
|
+
# — no long-lived signing key to leak. Verifiers can pin to
|
|
865
|
+
# \`--certificate-identity\` matching this workflow URL.
|
|
866
|
+
env:
|
|
867
|
+
COSIGN_EXPERIMENTAL: "1"
|
|
868
|
+
run: |
|
|
869
|
+
set -eu
|
|
870
|
+
cosign sign --yes "$IMAGE_REF"
|
|
871
|
+
|
|
872
|
+
- name: Generate SPDX SBOM for image
|
|
873
|
+
# SHA-pinned per the project's actions-pinning policy. v0.24.0,
|
|
874
|
+
# commit e22c389904149dbc22b58101806040fa8d37a610.
|
|
875
|
+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
|
876
|
+
with:
|
|
877
|
+
image: \${{ env.IMAGE_REF }}
|
|
878
|
+
format: spdx-json
|
|
879
|
+
output-file: sbom.spdx.json
|
|
880
|
+
upload-artifact: false
|
|
881
|
+
upload-release-assets: false
|
|
882
|
+
|
|
883
|
+
- name: Attest SBOM with Cosign
|
|
884
|
+
env:
|
|
885
|
+
COSIGN_EXPERIMENTAL: "1"
|
|
886
|
+
run: |
|
|
887
|
+
set -eu
|
|
888
|
+
cosign attest --yes \
|
|
889
|
+
--predicate sbom.spdx.json \
|
|
890
|
+
--type spdxjson \
|
|
891
|
+
"$IMAGE_REF"`;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function vercelDeployHeader() {
|
|
895
|
+
return `# Configure before the first run:
|
|
896
|
+
# - GitHub Actions secret: VERCEL_TOKEN
|
|
897
|
+
# - GitHub Actions variables or environment variables: VERCEL_ORG_ID and VERCEL_PROJECT_ID
|
|
898
|
+
# This workflow stays manual-only until you wire those in and decide whether
|
|
899
|
+
# you want automatic deploys from main.`;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function cloudflareDeployHeader() {
|
|
903
|
+
return `# Configure before the first run:
|
|
904
|
+
# - GitHub Actions secret: CLOUDFLARE_API_TOKEN
|
|
905
|
+
# - GitHub Actions variable or environment variable: CLOUDFLARE_ACCOUNT_ID
|
|
906
|
+
# Keep this manual-only until the worker is linked to the right account and
|
|
907
|
+
# the production environment has approval rules.`;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function providerDeploySetup({ packageManager, needsBunRuntime }) {
|
|
911
|
+
return joinWorkflowBlocks([
|
|
912
|
+
setupPackageManagerStep(packageManager),
|
|
913
|
+
setupNodeStep(),
|
|
914
|
+
needsBunRuntime ? setupBunStep() : "",
|
|
915
|
+
installDependenciesStep(installCommand(packageManager)),
|
|
916
|
+
]);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function vercelDeploySteps(packageManager) {
|
|
920
|
+
return ` - name: Deploy to Vercel
|
|
921
|
+
env:
|
|
922
|
+
VERCEL_TOKEN: \${{ secrets.VERCEL_TOKEN }}
|
|
923
|
+
VERCEL_ORG_ID: \${{ vars.VERCEL_ORG_ID }}
|
|
924
|
+
VERCEL_PROJECT_ID: \${{ vars.VERCEL_PROJECT_ID }}
|
|
925
|
+
run: |
|
|
926
|
+
set -eu
|
|
927
|
+
: "\${VERCEL_TOKEN:?Set the VERCEL_TOKEN Actions secret before running this workflow.}"
|
|
928
|
+
: "\${VERCEL_ORG_ID:?Set the VERCEL_ORG_ID Actions variable before running this workflow.}"
|
|
929
|
+
: "\${VERCEL_PROJECT_ID:?Set the VERCEL_PROJECT_ID Actions variable before running this workflow.}"
|
|
930
|
+
${execCommand(packageManager, "vercel", "deploy --prod --yes --token \"$VERCEL_TOKEN\"")}`;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function cloudflareDeploySteps(packageManager) {
|
|
934
|
+
return ` - name: Deploy to Cloudflare Workers
|
|
935
|
+
env:
|
|
936
|
+
CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
937
|
+
CLOUDFLARE_ACCOUNT_ID: \${{ vars.CLOUDFLARE_ACCOUNT_ID }}
|
|
938
|
+
run: |
|
|
939
|
+
set -eu
|
|
940
|
+
: "\${CLOUDFLARE_API_TOKEN:?Set the CLOUDFLARE_API_TOKEN Actions secret before running this workflow.}"
|
|
941
|
+
: "\${CLOUDFLARE_ACCOUNT_ID:?Set the CLOUDFLARE_ACCOUNT_ID Actions variable before running this workflow.}"
|
|
942
|
+
${execCommand(packageManager, "wrangler", "deploy")}`;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function renderDeployConfig({ template, packageManager, needsBunRuntime }) {
|
|
946
|
+
if (template === "vercel-edge") {
|
|
947
|
+
return {
|
|
948
|
+
header: vercelDeployHeader(),
|
|
949
|
+
jobName: "Deploy to Vercel",
|
|
950
|
+
jobPermissions: "",
|
|
951
|
+
setupSteps: providerDeploySetup({ packageManager, needsBunRuntime }),
|
|
952
|
+
steps: vercelDeploySteps(packageManager),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (template === "cloudflare-worker") {
|
|
956
|
+
return {
|
|
957
|
+
header: cloudflareDeployHeader(),
|
|
958
|
+
jobName: "Deploy to Cloudflare Workers",
|
|
959
|
+
jobPermissions: "",
|
|
960
|
+
setupSteps: providerDeploySetup({ packageManager, needsBunRuntime }),
|
|
961
|
+
steps: cloudflareDeploySteps(packageManager),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
header: containerRegistryHeader(),
|
|
966
|
+
jobName: "Publish container image",
|
|
967
|
+
// `packages: write` lets us push to GHCR; `id-token: write` lets
|
|
968
|
+
// Cosign mint a short-lived OIDC identity for keyless image signing
|
|
969
|
+
// + SBOM attestation. Both are scoped to this single job — the
|
|
970
|
+
// top-level workflow still has `permissions: {}`.
|
|
971
|
+
jobPermissions: " packages: write\n id-token: write",
|
|
972
|
+
setupSteps: "",
|
|
973
|
+
steps: containerPublishSteps(),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
743
977
|
function workflowStep(name, command) {
|
|
744
978
|
return ` - name: ${name}
|
|
745
979
|
run: ${command}`;
|
|
@@ -763,13 +997,21 @@ async function addLockfileVerifyScript(dir) {
|
|
|
763
997
|
await writePackageJson(dir, packageJson);
|
|
764
998
|
}
|
|
765
999
|
|
|
766
|
-
function renderCiReplacements({ packageManager, template, packageJson, codeOwner }) {
|
|
1000
|
+
function renderCiReplacements({ packageManager, template, packageJson, codeOwner, includeSecurityBundle = true }) {
|
|
767
1001
|
const setupPm = setupPackageManagerStep(packageManager);
|
|
768
1002
|
const needsBunRuntime = template === "bun-basic" && packageManager !== "bun";
|
|
769
1003
|
const audit = auditCommand(packageManager);
|
|
770
1004
|
const auditFull = auditFullCommand(packageManager);
|
|
771
1005
|
const buildStep = hasPackageScript(packageJson, "build") ? workflowStep("Build", runScriptCommand(packageManager, "build")) : "";
|
|
772
1006
|
const auditStep = audit ? workflowStep(auditStepName(packageManager), audit) : "";
|
|
1007
|
+
const deploy = renderDeployConfig({ template, packageManager, needsBunRuntime });
|
|
1008
|
+
// The verify:lockfile script is only scaffolded when the security bundle
|
|
1009
|
+
// ships. In a deploy-only scaffold the script does not exist on disk, so
|
|
1010
|
+
// the deploy workflow must omit the step instead of failing fast on a
|
|
1011
|
+
// missing file.
|
|
1012
|
+
const deployVerifyLockfileStep = includeSecurityBundle
|
|
1013
|
+
? workflowStep("Verify lockfile sources", runScriptCommand(packageManager, "verify:lockfile"))
|
|
1014
|
+
: "";
|
|
773
1015
|
// vuln-scan.yml: production-tree audit is blocking, full-tree audit is
|
|
774
1016
|
// advisory (continue-on-error) so a low-severity dev-tool advisory does
|
|
775
1017
|
// not page the on-call on a daily cron.
|
|
@@ -781,6 +1023,13 @@ function renderCiReplacements({ packageManager, template, packageJson, codeOwner
|
|
|
781
1023
|
: "";
|
|
782
1024
|
|
|
783
1025
|
return new Map([
|
|
1026
|
+
["__DEPLOY_HEADER__", deploy.header],
|
|
1027
|
+
["__DEPLOY_JOB_NAME__", deploy.jobName],
|
|
1028
|
+
["__DEPLOY_JOB_PERMISSIONS__", deploy.jobPermissions],
|
|
1029
|
+
["__DEPLOY_SETUP_STEPS__", deploy.setupSteps],
|
|
1030
|
+
["__DEPLOY_STEPS__", deploy.steps],
|
|
1031
|
+
["__DEPLOY_VERIFY_LOCKFILE_STEP__", deployVerifyLockfileStep],
|
|
1032
|
+
["__DEPLOY_REF_GUARD__", deployRefGuard()],
|
|
784
1033
|
["__CODE_OWNER__", codeOwner],
|
|
785
1034
|
["__SETUP_PACKAGE_MANAGER_STEP__", setupPm],
|
|
786
1035
|
["__SETUP_BUN_RUNTIME_STEP__", needsBunRuntime ? setupBunStep() : ""],
|
|
@@ -817,7 +1066,35 @@ async function replacePlaceholdersInTree(dir, replacements) {
|
|
|
817
1066
|
}
|
|
818
1067
|
}
|
|
819
1068
|
|
|
820
|
-
async function
|
|
1069
|
+
async function pruneCiBundle(targetDir, flavor, { includeSecurityBundle, includeDeployWorkflow }) {
|
|
1070
|
+
if (!includeSecurityBundle) {
|
|
1071
|
+
const workflowFiles = flavor === "deno"
|
|
1072
|
+
? ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "opengrep.yml", "osv-scan.yml", "scorecard.yml", "secret-scan.yml", "zizmor.yml"]
|
|
1073
|
+
: ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "opengrep.yml", "osv-scan.yml", "scorecard.yml", "secret-scan.yml", "vuln-scan.yml", "zizmor.yml"];
|
|
1074
|
+
for (const file of workflowFiles) {
|
|
1075
|
+
await rm(path.join(targetDir, ".github", "workflows", file), { force: true });
|
|
1076
|
+
}
|
|
1077
|
+
await rm(path.join(targetDir, ".github", "dependabot.yml"), { force: true });
|
|
1078
|
+
await rm(path.join(targetDir, ".github", "CODEOWNERS"), { force: true });
|
|
1079
|
+
await rm(path.join(targetDir, "SECURITY.md"), { force: true });
|
|
1080
|
+
if (flavor === "node") {
|
|
1081
|
+
await rm(path.join(targetDir, "scripts", "verify-lockfile-sources.mjs"), { force: true });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (!includeDeployWorkflow) {
|
|
1086
|
+
await rm(path.join(targetDir, ".github", "workflows", "deploy.yml"), { force: true });
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function copyCiBundle(
|
|
1091
|
+
targetDir,
|
|
1092
|
+
template,
|
|
1093
|
+
packageManager,
|
|
1094
|
+
skipPackageManager,
|
|
1095
|
+
codeOwner,
|
|
1096
|
+
{ includeSecurityBundle = true, includeDeployWorkflow = true } = {},
|
|
1097
|
+
) {
|
|
821
1098
|
const flavor = skipPackageManager ? "deno" : "node";
|
|
822
1099
|
const sourceDir = path.join(CI_TEMPLATES_DIR, flavor);
|
|
823
1100
|
if (!existsSync(sourceDir)) {
|
|
@@ -825,23 +1102,42 @@ async function copyCiBundle(targetDir, template, packageManager, skipPackageMana
|
|
|
825
1102
|
}
|
|
826
1103
|
await copyTemplate(sourceDir, targetDir);
|
|
827
1104
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
);
|
|
1105
|
+
await pruneCiBundle(targetDir, flavor, { includeSecurityBundle, includeDeployWorkflow });
|
|
1106
|
+
|
|
1107
|
+
if (!includeSecurityBundle && !includeDeployWorkflow) {
|
|
1108
|
+
return;
|
|
833
1109
|
}
|
|
1110
|
+
|
|
1111
|
+
const candidate = codeOwner?.trim() ?? "";
|
|
834
1112
|
const owner = candidate || "@your-org/security-team";
|
|
835
1113
|
if (skipPackageManager) {
|
|
1114
|
+
if (includeSecurityBundle && candidate && !VALID_CODE_OWNER.test(candidate)) {
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
`Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
836
1119
|
await replacePlaceholdersInTree(targetDir, new Map([["__CODE_OWNER__", owner]]));
|
|
837
1120
|
return;
|
|
838
1121
|
}
|
|
839
1122
|
|
|
840
|
-
|
|
1123
|
+
if (includeSecurityBundle) {
|
|
1124
|
+
if (candidate && !VALID_CODE_OWNER.test(candidate)) {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
`Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
await addLockfileVerifyScript(targetDir);
|
|
1130
|
+
}
|
|
841
1131
|
const packageJson = await readPackageJsonIfPresent(targetDir);
|
|
842
1132
|
await replacePlaceholdersInTree(
|
|
843
1133
|
targetDir,
|
|
844
|
-
renderCiReplacements({
|
|
1134
|
+
renderCiReplacements({
|
|
1135
|
+
packageManager,
|
|
1136
|
+
template,
|
|
1137
|
+
packageJson,
|
|
1138
|
+
codeOwner: owner,
|
|
1139
|
+
includeSecurityBundle,
|
|
1140
|
+
}),
|
|
845
1141
|
);
|
|
846
1142
|
}
|
|
847
1143
|
|
|
@@ -1213,7 +1509,7 @@ function createSpinner(initialMessage) {
|
|
|
1213
1509
|
};
|
|
1214
1510
|
}
|
|
1215
1511
|
|
|
1216
|
-
function printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi }) {
|
|
1512
|
+
function printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi, withDeploy }) {
|
|
1217
1513
|
const templateMeta = TEMPLATE_OPTIONS.find((option) => option.value === template);
|
|
1218
1514
|
const templateLabel = templateMeta ? `${templateMeta.title} ${color(COLORS.dim, `(${template})`)}` : template;
|
|
1219
1515
|
const summaryLines = [
|
|
@@ -1230,6 +1526,9 @@ function printSummary({ projectName, template, packageManager, installDeps, skip
|
|
|
1230
1526
|
if (withCi) {
|
|
1231
1527
|
summaryLines.push(`${color(COLORS.gray, "Security ")} ${color(COLORS.cyan, "GitHub CI bundle")}`);
|
|
1232
1528
|
}
|
|
1529
|
+
if (withDeploy) {
|
|
1530
|
+
summaryLines.push(`${color(COLORS.gray, "Deploy ")} ${color(COLORS.cyan, "Starter workflow")}`);
|
|
1531
|
+
}
|
|
1233
1532
|
console.log("");
|
|
1234
1533
|
console.log(renderBox(summaryLines, { accent: COLORS.green }));
|
|
1235
1534
|
console.log("");
|
|
@@ -1254,10 +1553,13 @@ function printSummary({ projectName, template, packageManager, installDeps, skip
|
|
|
1254
1553
|
` ${color(COLORS.gray, SYMBOLS.pointer)} ${color(COLORS.dim, "(including a just-released @daloyjs/core) are embargoed for 24 h.")}`,
|
|
1255
1554
|
);
|
|
1256
1555
|
console.log(
|
|
1257
|
-
` ${color(COLORS.gray, SYMBOLS.pointer)} ${color(COLORS.dim, "Lifecycle scripts
|
|
1556
|
+
` ${color(COLORS.gray, SYMBOLS.pointer)} ${color(COLORS.dim, "Lifecycle scripts stay blocked by default (ignore-scripts=true);")}`,
|
|
1557
|
+
);
|
|
1558
|
+
console.log(
|
|
1559
|
+
` ${color(COLORS.gray, SYMBOLS.pointer)} ${color(COLORS.dim, "if you later tighten build-script policy, keep exceptions in")}`,
|
|
1258
1560
|
);
|
|
1259
1561
|
console.log(
|
|
1260
|
-
` ${color(COLORS.gray, SYMBOLS.pointer)} ${color(COLORS.dim, "
|
|
1562
|
+
` ${color(COLORS.gray, SYMBOLS.pointer)} ${color(COLORS.dim, "pnpm-workspace.yaml under allowBuilds instead of weakening .npmrc.")}`,
|
|
1261
1563
|
);
|
|
1262
1564
|
}
|
|
1263
1565
|
|
|
@@ -1378,18 +1680,17 @@ async function main() {
|
|
|
1378
1680
|
} else if (packageManager === "pnpm") {
|
|
1379
1681
|
// Deny-by-default for pnpm: the scaffolded `pnpm-workspace.yaml` ships
|
|
1380
1682
|
// with `minimumReleaseAge: 1440` (24 h embargo on newly-published
|
|
1381
|
-
// versions) and the `.npmrc`
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1385
|
-
//
|
|
1386
|
-
//
|
|
1387
|
-
// makes that explicit instead of failing the install silently.
|
|
1683
|
+
// versions), and the scaffolded `.npmrc` keeps `ignore-scripts=true`.
|
|
1684
|
+
// Both are security best practices, but they mean the first install
|
|
1685
|
+
// should be a deliberate user choice rather than something the CLI
|
|
1686
|
+
// does implicitly. Defaulting to N makes that explicit, and it also
|
|
1687
|
+
// avoids surprising users when a fresh `@daloyjs/core` release is
|
|
1688
|
+
// still inside the 24 h embargo window.
|
|
1388
1689
|
if (rl) {
|
|
1389
1690
|
console.log(
|
|
1390
1691
|
color(
|
|
1391
1692
|
COLORS.gray,
|
|
1392
|
-
" (pnpm install may
|
|
1693
|
+
" (pnpm install may wait out a fresh-release embargo and keeps lifecycle scripts blocked by default \u2014 see .npmrc + pnpm-workspace.yaml)",
|
|
1393
1694
|
),
|
|
1394
1695
|
);
|
|
1395
1696
|
installDeps = await askYesNo(rl, `Install dependencies with ${packageManager}?`, false);
|
|
@@ -1413,6 +1714,15 @@ async function main() {
|
|
|
1413
1714
|
withCi = rl ? await askYesNo(rl, "Add hardened GitHub Actions and security files?", true) : true;
|
|
1414
1715
|
}
|
|
1415
1716
|
|
|
1717
|
+
let withDeploy = opts.deploy;
|
|
1718
|
+
if (withDeploy === undefined) {
|
|
1719
|
+
if (withCi) {
|
|
1720
|
+
withDeploy = true;
|
|
1721
|
+
} else {
|
|
1722
|
+
withDeploy = rl ? await askYesNo(rl, "Add a starter deployment workflow?", false) : false;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1416
1726
|
rl?.close();
|
|
1417
1727
|
|
|
1418
1728
|
if (interactive) {
|
|
@@ -1443,9 +1753,18 @@ async function main() {
|
|
|
1443
1753
|
}
|
|
1444
1754
|
}
|
|
1445
1755
|
|
|
1446
|
-
if (withCi) {
|
|
1447
|
-
await copyCiBundle(targetDir, template, packageManager, skipPackageManager, opts.codeOwner
|
|
1448
|
-
|
|
1756
|
+
if (withCi || withDeploy) {
|
|
1757
|
+
await copyCiBundle(targetDir, template, packageManager, skipPackageManager, opts.codeOwner, {
|
|
1758
|
+
includeSecurityBundle: withCi,
|
|
1759
|
+
includeDeployWorkflow: withDeploy,
|
|
1760
|
+
});
|
|
1761
|
+
if (withCi && withDeploy) {
|
|
1762
|
+
logStep("GitHub automation added", `${skipPackageManager ? "deno" : packageManager} + deploy`);
|
|
1763
|
+
} else if (withCi) {
|
|
1764
|
+
logStep("GitHub security bundle added", skipPackageManager ? "deno" : packageManager);
|
|
1765
|
+
} else if (withDeploy) {
|
|
1766
|
+
logStep("Deploy starter added", template);
|
|
1767
|
+
}
|
|
1449
1768
|
}
|
|
1450
1769
|
|
|
1451
1770
|
if (initGit) {
|
|
@@ -1474,7 +1793,7 @@ async function main() {
|
|
|
1474
1793
|
}
|
|
1475
1794
|
}
|
|
1476
1795
|
|
|
1477
|
-
printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi });
|
|
1796
|
+
printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi, withDeploy });
|
|
1478
1797
|
} catch (err) {
|
|
1479
1798
|
rl?.close();
|
|
1480
1799
|
if (err && err.message === "Cancelled") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-daloy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,4 +45,4 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"test": "node --test test/**/*.test.mjs"
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|