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:
Anso
2026-05-02 00:49:49 -04:00
committed by GitHub
parent 7cde9917a5
commit 5e29649f3e
11 changed files with 1268 additions and 15 deletions
+11
View File
@@ -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.
+31
View File
@@ -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
+54 -10
View File
@@ -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') }}
+48
View File
@@ -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