mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
chore: workflow hardening (husky+commitlint, dependency-review, stale, GHCR, mesh-sidecar digest) (#863)
* chore(repo): enforce Conventional Commits via husky and commitlint release-please parses commit subjects on main to compute the next version and regenerate CHANGELOG.md. A non-conforming commit silently breaks both, so the format must be enforced at commit time, not by review. Adds husky 9 to wire a commit-msg hook, commitlint with the conventional config, and a small rule override (loosen subject length to 120 chars, disable subject-case so existing imperative subjects keep passing). The prepare script runs husky on npm install so contributors do not need to configure it manually. * ci: add dependency-review workflow GitHub-native action that diffs the PR's manifests (package.json, package-lock.json) against main and fails when a new transitive dep introduces a high or critical CVE. Existing vulnerabilities tracked in security/vex/sencho.openvex.json and the Trivy scan in ci.yml are not re-flagged here. Pinned to actions/dependency-review-action v4.9.0 by commit SHA. Comment summaries are posted to the PR only on failure to keep the conversation clean on green PRs. * ci: add stale workflow for issues and pull requests Marks issues and PRs as stale after 60 days of inactivity and closes 14 days later unless re-engaged. Issues labeled pinned, security, bug, or tracking are exempt and never auto-closed; PRs labeled pinned or security are exempt. Runs daily at 01:30 UTC and processes up to 30 items per run to stay within the action's rate budget. Pinned to actions/stale v10.2.0 by commit SHA. * chore(repo): route new issues to docs, discussions, and security policy Disables the blank-issue option and adds three contact links the issue chooser surfaces above the bug-report and feature-request templates: docs.sencho.io for setup questions, GitHub Discussions for open-ended chat, and the repository security policy for private vulnerability reports. This keeps the bug tracker focused on actionable bug reports and feature requests instead of support questions. * ci(docker): publish multi-arch image to GHCR alongside Docker Hub Mirrors every released sencho and sencho-mesh image to ghcr.io with the same tags, signing, and supply-chain attestations as the Docker Hub copy. Same content; pull from whichever registry your environment prefers. - Adds packages:write to both publish jobs so the auto-provisioned GITHUB_TOKEN can push to ghcr.io/studio-saelix/sencho and -mesh. - Adds a second docker/login-action step authenticating to ghcr.io. The Docker Hub login still resolves credentials from the production env. - docker/metadata-action now lists both image references; one buildx push attaches the manifest to both registries in a single round trip. - Cosign keyless signing already loops over $TAGS, so adding the GHCR reference is enough to sign both digests. - The cosign attest step now loops over both registry paths so SBOM (CDX + SPDX) and OpenVEX attestations are resolvable from either registry. Quickstart and image-verification docs note GHCR as an alternative pull source with identical content. * fix(mesh-sidecar): pin base image by digest The main Sencho Dockerfile pins every base image by sha256 digest so a republish of an upstream tag cannot silently shift content into a release. The mesh-sidecar Dockerfile pinned node:22-alpine by tag, which is exactly the gap that pinning closes. Resolves node:22-alpine to its current multi-arch index digest (covers linux/amd64 and linux/arm64) and threads it through both build stages via a single ARG so a future digest roll only edits one line. The inline comment documents the resolution command. The sidecar holds an outbound websocket and has no inbound HTTP listener, so adding a HEALTHCHECK is intentionally out of scope: a process-liveness probe would be tautological with Docker's restart policy. The Dockerfile now records that decision so it does not get reopened on every audit. * ci(docker): use repository_owner for GHCR login username github.actor varies by event source (the maintainer who merged the release PR on tag pushes, github-actions[bot] on bot-driven runs, the dispatcher on workflow_dispatch). The username field is metadata only; GITHUB_TOKEN is what authenticates against GHCR. github.repository_owner resolves to a fixed value (studio-saelix) on every event and matches GitHub's own example workflows for GHCR push. * chore(repo): tighten commit subject-case rule Disabling subject-case entirely allowed accidental ALL-CAPS or PascalCase subjects through. Restrict to "never upper-case or pascal-case" instead, which preserves the lowercase / kebab-case norm of this repo and lets sentence-case subjects (used sparingly on main) keep passing.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://docs.sencho.io
|
||||
about: Setup, configuration, and feature documentation. Most "how do I" questions are answered here.
|
||||
- name: Discussions
|
||||
url: https://github.com/Studio-Saelix/sencho/discussions
|
||||
about: Ask a question, share a setup, or propose an idea before opening an issue.
|
||||
- name: Security policy
|
||||
url: https://github.com/Studio-Saelix/sencho/security/policy
|
||||
about: Privately report security vulnerabilities. Do not open a public issue for security reports.
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Dependency Review
|
||||
|
||||
# Blocks PRs that introduce new transitive dependencies with known
|
||||
# high-or-critical CVEs. The check runs against the diff of the PR's
|
||||
# manifest files (package.json / package-lock.json) and the base branch,
|
||||
# so existing vulnerabilities (tracked in security/vex/sencho.openvex.json
|
||||
# and Trivy) are not re-flagged here.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: dependency-review-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Run dependency review
|
||||
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||
with:
|
||||
fail-on-severity: high
|
||||
comment-summary-in-pr: on-failure
|
||||
@@ -12,7 +12,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
name: Push Docker image to Docker Hub and GHCR
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
# DOCKERHUB_USERNAME and DOCKERHUB_TOKEN live in the `production` environment,
|
||||
@@ -27,6 +27,10 @@ jobs:
|
||||
# Required for cosign keyless signing via GitHub OIDC and for uploading
|
||||
# SBOM/VEX files to GitHub Releases via softprops/action-gh-release.
|
||||
id-token: write
|
||||
# Required to push the multi-arch image to ghcr.io/studio-saelix/sencho
|
||||
# using the auto-provisioned GITHUB_TOKEN. Docker Hub credentials still
|
||||
# come from the production environment above.
|
||||
packages: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
@@ -54,6 +58,17 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
# repository_owner resolves to a fixed value (studio-saelix) on every
|
||||
# event type. github.actor varies (a maintainer on workflow_dispatch,
|
||||
# github-actions[bot] on the release tag push) and is just a label;
|
||||
# GITHUB_TOKEN is what actually authenticates.
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
|
||||
@@ -61,7 +76,12 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
with:
|
||||
images: saelix/sencho
|
||||
# Publish the same manifest under both registries so users on either
|
||||
# platform can pull. Docker Hub remains primary for discoverability;
|
||||
# GHCR mirrors at ghcr.io/studio-saelix/sencho with identical tags.
|
||||
images: |
|
||||
saelix/sencho
|
||||
ghcr.io/studio-saelix/sencho
|
||||
# On a v-tag push we publish:
|
||||
# latest always points at the newest release
|
||||
# X.Y.Z the immutable semver tag
|
||||
@@ -212,15 +232,22 @@ jobs:
|
||||
|
||||
- name: Attest SBOMs and VEX with cosign (keyless)
|
||||
# Attaches CycloneDX SBOM, SPDX SBOM, and OpenVEX document as signed
|
||||
# OCI referrer attestations on the published digest. Verification
|
||||
# commands are documented in docs/operations/verifying-images.mdx.
|
||||
# OCI referrer attestations on the published digest, separately to each
|
||||
# registry path. Attestations live next to the image manifest in the
|
||||
# registry, so a verifier pulling from GHCR cannot resolve attestations
|
||||
# written only to Docker Hub. Verification commands are documented in
|
||||
# docs/operations/verifying-images.mdx.
|
||||
env:
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
run: |
|
||||
IMAGE_REF="saelix/sencho@${DIGEST}"
|
||||
cosign attest --yes --predicate sbom.cdx.json --type cyclonedx "${IMAGE_REF}"
|
||||
cosign attest --yes --predicate sbom.spdx.json --type spdxjson "${IMAGE_REF}"
|
||||
cosign attest --yes --predicate security/vex/sencho.openvex.json --type openvex "${IMAGE_REF}"
|
||||
for IMAGE_REF in \
|
||||
"saelix/sencho@${DIGEST}" \
|
||||
"ghcr.io/studio-saelix/sencho@${DIGEST}"
|
||||
do
|
||||
cosign attest --yes --predicate sbom.cdx.json --type cyclonedx "${IMAGE_REF}"
|
||||
cosign attest --yes --predicate sbom.spdx.json --type spdxjson "${IMAGE_REF}"
|
||||
cosign attest --yes --predicate security/vex/sencho.openvex.json --type openvex "${IMAGE_REF}"
|
||||
done
|
||||
|
||||
- name: Upload SBOM and VEX to GitHub Release
|
||||
# Fallback for consumers who do not use cosign; files are also
|
||||
@@ -234,13 +261,15 @@ jobs:
|
||||
security/vex/sencho.openvex.json
|
||||
|
||||
push_mesh_sidecar:
|
||||
name: Push Sencho Mesh sidecar image to Docker Hub
|
||||
name: Push Sencho Mesh sidecar image to Docker Hub and GHCR
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
environment: production
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
# Required to push the sidecar image to ghcr.io/studio-saelix/sencho-mesh.
|
||||
packages: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
@@ -259,6 +288,17 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
# repository_owner resolves to a fixed value (studio-saelix) on every
|
||||
# event type. github.actor varies (a maintainer on workflow_dispatch,
|
||||
# github-actions[bot] on the release tag push) and is just a label;
|
||||
# GITHUB_TOKEN is what actually authenticates.
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
|
||||
@@ -268,7 +308,11 @@ jobs:
|
||||
with:
|
||||
# Sencho Mesh sidecar image. Versioned in lockstep with the main
|
||||
# sencho image so each Sencho release has a matched mesh sidecar.
|
||||
images: saelix/sencho-mesh
|
||||
# Same registry pair as the main image; tags are identical across
|
||||
# both so users on either registry pull the same content.
|
||||
images: |
|
||||
saelix/sencho-mesh
|
||||
ghcr.io/studio-saelix/sencho-mesh
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Stale Issues and Pull Requests
|
||||
|
||||
# Marks issues and PRs as stale after 60 days of inactivity and closes
|
||||
# them 14 days after that unless re-engaged. Keeps the issue tracker
|
||||
# focused on actionable work without losing real bugs: anything labeled
|
||||
# pinned, security, bug, or tracking is exempt and never auto-closed.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Daily at 01:30 UTC. Off-peak for the GitHub Actions stale runner.
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: stale
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
with:
|
||||
days-before-stale: 60
|
||||
days-before-close: 14
|
||||
stale-issue-label: stale
|
||||
stale-pr-label: stale
|
||||
exempt-issue-labels: 'pinned,security,bug,tracking'
|
||||
exempt-pr-labels: 'pinned,security'
|
||||
stale-issue-message: >
|
||||
This issue has had no activity for 60 days and will be closed in
|
||||
14 days unless someone replies. Add a comment or remove the stale
|
||||
label to keep it open.
|
||||
close-issue-message: >
|
||||
Closing for inactivity. Reopen if this is still relevant.
|
||||
stale-pr-message: >
|
||||
This pull request has had no activity for 60 days and will be
|
||||
closed in 14 days unless updated. Push a new commit or remove the
|
||||
stale label to keep it open.
|
||||
close-pr-message: >
|
||||
Closing for inactivity. Reopen and rebase if you want to continue.
|
||||
# Process up to 30 items per run to stay within the action's rate
|
||||
# budget; running daily covers the steady state easily.
|
||||
operations-per-run: 30
|
||||
@@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit "$1"
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Conventional Commits validation for Sencho. Subject lines must match
|
||||
* <type>(<optional scope>): <subject>
|
||||
* with type drawn from the allow-list below. release-please reads commits
|
||||
* with these types from main and computes the next version + changelog,
|
||||
* so any non-conforming commit silently breaks the release pipeline.
|
||||
*
|
||||
* Scope is free-form; common scopes used in this repo include backend,
|
||||
* frontend, e2e, mesh-sidecar, docs, deps, ci, license, blueprints,
|
||||
* fleet, security.
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'revert',
|
||||
'docs',
|
||||
'style',
|
||||
'refactor',
|
||||
'test',
|
||||
'build',
|
||||
'ci',
|
||||
'chore',
|
||||
'security',
|
||||
],
|
||||
],
|
||||
// The default 100-char subject limit is too tight for the descriptive
|
||||
// subjects this repo prefers; loosen to 120 to match the longest
|
||||
// existing commit subjects on main without enabling unbounded sprawl.
|
||||
'header-max-length': [2, 'always', 120],
|
||||
// Allow lowercase and kebab-case subjects (the repo norm), block
|
||||
// accidental ALL-CAPS or PascalCase. Looser than the default which
|
||||
// also banned sentence-case and start-case; existing commits on main
|
||||
// use sentence-case sparingly and we do not want to retro-block them.
|
||||
'subject-case': [2, 'never', ['upper-case', 'pascal-case']],
|
||||
},
|
||||
};
|
||||
@@ -27,6 +27,10 @@ Open `http://localhost:1852` in your browser. On first boot you'll be prompted t
|
||||
Replace `/opt/compose` with the path to your Compose projects directory. Every subdirectory inside it becomes a stack in Sencho. A `JWT_SECRET` is generated automatically on first boot; you do not need to provide one.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
Sencho is also published on GitHub Container Registry at `ghcr.io/studio-saelix/sencho:latest` with the same tags and content as Docker Hub. Pull from whichever registry your environment prefers.
|
||||
</Tip>
|
||||
|
||||
## Important: the 1:1 path rule
|
||||
|
||||
The compose directory must be mounted at the **same path** inside and outside the container. The example above mounts `/opt/compose` to `/opt/compose`, which is correct. If your stacks live at a different path, adjust both sides of the mount to match. See the [Configuration guide](/getting-started/configuration#compose-directory-the-11-path-rule) for details.
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Verifying Published Images
|
||||
description: How to verify signatures, provenance, SBOMs, and CVE triage for Sencho Docker images.
|
||||
---
|
||||
|
||||
Every Sencho release image published to Docker Hub is signed and carries verifiable supply-chain artifacts. This page explains how to check each one.
|
||||
Every Sencho release image is published to two registries with identical content: Docker Hub (`saelix/sencho`) and GitHub Container Registry (`ghcr.io/studio-saelix/sencho`). Every image is signed and carries verifiable supply-chain artifacts on both registries. The `cosign verify` examples below use the Docker Hub path; substitute `ghcr.io/studio-saelix/sencho:<tag>` to verify the GHCR copy.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
+15
-2
@@ -1,17 +1,30 @@
|
||||
# Multi-stage build for the Sencho Mesh sidecar.
|
||||
FROM node:22-alpine AS build
|
||||
#
|
||||
# Base image is pinned by digest, not tag, so a future republish of the
|
||||
# `22-alpine` tag cannot silently shift the runtime under a release. Update
|
||||
# the digest in the same PR that rolls the Node minor or upgrades from
|
||||
# alpine. Resolve a fresh digest with:
|
||||
# docker buildx imagetools inspect node:22-alpine
|
||||
ARG NODE_BASE=node:22-alpine@sha256:8ea2348b068a9544dae7317b4f3aafcdc032df1647bb7d768a05a5cad1a7683f
|
||||
|
||||
FROM ${NODE_BASE} AS build
|
||||
WORKDIR /app
|
||||
COPY package.json tsconfig.json ./
|
||||
RUN npm install --no-audit --no-fund
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine AS runtime
|
||||
FROM ${NODE_BASE} AS runtime
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY package.json ./
|
||||
RUN npm install --omit=dev --no-audit --no-fund
|
||||
COPY --from=build /app/dist ./dist
|
||||
|
||||
# No HEALTHCHECK: the sidecar has no inbound HTTP listener. It maintains
|
||||
# an outbound websocket to the control plane and exits on connection loss
|
||||
# so Docker's restart policy is the natural recovery loop. A liveness
|
||||
# probe based on the node process being PID 1 would be tautological.
|
||||
|
||||
USER node
|
||||
CMD ["node", "dist/index.js"]
|
||||
|
||||
Generated
+1054
File diff suppressed because it is too large
Load Diff
+6
-2
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"test": "cd backend && npm test",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,7 +22,10 @@
|
||||
},
|
||||
"homepage": "https://github.com/studio-saelix/sencho#readme",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.5.3",
|
||||
"@commitlint/config-conventional": "^20.5.3",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"husky": "^9.1.7",
|
||||
"otplib": "^13.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user