mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-28 19:27:41 +00:00
392bc15d91
* feat(git): swap isomorphic-git for native git transport behind clone seam Replace the isomorphic-git engine (HTTP-only, single importer) with the native git CLI behind the existing withClonedRepo seam, so SSH deploy keys, ref semantics, and private CAs become reachable in later PRs. - resolve-before-fetch: ls-remote pins the branch to an immutable SHA, then rev-parse verifies the checkout against it; tip races refuse - hardened spawns: argv arrays only, protocol allowlist (https only), neutralized hooks, isolated HOME and all config channels, no prompts - token reaches git only via a credential helper reading SENCHO_GIT_TOKEN from the child env; never argv or URL - size cap becomes a workspace watchdog (on-disk measure) keeping the same knob and breach message; deterministic final gate added - Windows: pin http.sslBackend=openssl (schannel ignores sslCAInfo) and anchor to Git's bundled CA; NODE_EXTRA_CA_CERTS combines with platform defaults instead of replacing them - error classification retargets to exit code + stderr while preserving the contractual mappings (AUTH_FAILED maps to 400, never 401; unauthenticated refusals mask as REPO_NOT_FOUND) - runtime image installs git; tests re-pointed at the transport boundary plus a new engine suite (classifier corpus, argv hardening, watchdog) Zero externally visible behavior change except two edge cases: an empty branch now surfaces BRANCH_NOT_FOUND, and a mid-fetch force push refuses instead of materializing the moved tip. * fix(git): unblock CI on linux kill-path test and codeql log warning Two CI-only findings from the first pipeline run: - The scripted spawn child in the transport tests lacked the kill method that killTree's POSIX fallback reaches when a fake process group does not exist; Linux runs crashed inside the timeout tests while Windows (taskkill branch) could not reproduce it. Give the fixture the method the real ChildProcess always has. - CodeQL flagged the workspace-removal warning that interpolated the NODE_EXTRA_CA_CERTS path (environment-sourced values are treated as sensitive at log sinks). Reword the warning to name the variable instead of its value; operators know their own environment. * fix(git): collapse remaining duplicated test setup so the shared helper is used * fix(git): close watchdog, size-gate, ref-validator, and kill-ordering gaps in native transport Resolves the release-blocking findings from an independent pre-merge audit of the native git transport swap: - A watchdog-triggered kill mid-clone was misclassified as a generic exit failure instead of a size breach, because runGit resolves (not rejects) when the child is killed via SIGKILL. - The final on-disk size measurement failed open when it could not be read (workspace removed mid-walk, permissions), letting an unmeasured clone through as a success. Now fails closed and logs the real cause. - The ref-name validator was an overly restrictive allow-list that rejected valid branch names (leading underscore, non-ASCII, '#'). Replaced with a deny-list matching real `git check-ref-format --branch` semantics, verified against the git binary, including a per-path-segment `.lock` check the first pass missed. - runGit's timeout handler settled as soon as a kill was issued rather than confirmed, racing workspace cleanup against a still-alive child tree. It now waits for the child's close event, with a bounded fallback if termination is never confirmed, and preserves the timeout classification if 'error' fires after the kill. - Windows killTree now also falls back to child.kill() when taskkill itself exits non-zero, not just when it fails to spawn. - Added a real, non-mocked integration test that drives the credential helper through the actual git binary against a local HTTPS server with Basic Auth checking. It caught a genuine bug the mocked suite could not see: the credential.helper config value was quoted in a way that broke git's own absolute-path helper detection, failing every authenticated clone. Fixed by removing the quotes. - Migrated a separately developed test file's mocks off the deleted isomorphic-git module onto the native transport seam, matching the pattern already used elsewhere, after merging with main pulled in that feature. Also updates two stale comments left over from the isomorphic-git era and adds a git version check to the Docker runtime image smoke tests. * fix(git): make credential-helper path safe, unify ref length, and fix Windows kill ordering Addresses three PR 1 correction items from pre-merge audit: - credential.helper is a shell string, not argv: interpolating the helper's workspace-relative path broke authenticated fetches whenever the workspace sat under a directory with a space in its name. The config value is now a fixed string that names an environment variable instead, so no workspace path character can affect how git's shell parses it. - The transport rejected branch names over 200 characters while the route accepted up to 256 and real git has no comparable limit. REF_MAX_LEN is now a single exported constant shared by the transport and both routes. - On Windows, taskkill runs as a separate process and could still be walking a killed process tree after the direct git child reported closed, letting the caller delete the workspace early. Kill operations are now awaited to completion (bounded by a timeout) before a timed-out or size-breached run settles, on both the close and error event paths. Verified against a real authenticated git server inside the built runtime image: public HTTPS, private HTTPS with a valid PAT, invalid PAT, a deleted branch, an oversized repository, and the awkward workspace-path case, including from a workspace path containing spaces and shell metacharacters. * fix(git): reap killed helpers and classify curl refusals
166 lines
7.4 KiB
YAML
166 lines
7.4 KiB
YAML
name: Build and Publish Dev Image
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Integration (dev) build vs release build
|
|
# ---------------------------------------------------------------------------
|
|
# This workflow produces the INTEGRATION image: the running artifact of main
|
|
# HEAD, published so a maintainer can pull and smoke-test the exact code queued
|
|
# for the next release BEFORE it is ever promoted to a public release tag.
|
|
#
|
|
# Integration build (this file) Release build (docker-publish.yml)
|
|
# ---------------------------------- -----------------------------------
|
|
# trigger: push to main trigger: push of a v* tag
|
|
# registry: GHCR only registries: Docker Hub + GHCR
|
|
# image: ghcr.io/studio-saelix/ images: saelix/sencho (Docker Hub)
|
|
# sencho-dev ghcr.io/studio-saelix/sencho
|
|
# tags: :dev (moving) + :dev-<sha> tags: latest, X.Y.Z, X.Y
|
|
# signing / SBOM / VEX / Release: no signing / SBOM / VEX / Release: yes
|
|
# approval gate: none approval gate: production environment
|
|
#
|
|
# Public release tags (latest, semver, moving-minor), Docker Hub publishing,
|
|
# cosign signatures, SBOM/VEX attestations, and GitHub Releases are RELEASE-ONLY
|
|
# and live exclusively in docker-publish.yml. The dev tags here are never
|
|
# promoted: `:dev` is a moving pointer at main HEAD, and `:dev-<short-sha>` pins
|
|
# one build for reproducible testing on a host. Nothing in this file touches
|
|
# Docker Hub or the `latest` tag.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
# A moving `:dev` tag only ever needs to reflect the newest main commit, so a
|
|
# superseded in-flight build can be cancelled. Ref-scoped to match the rest of
|
|
# the workflow suite.
|
|
group: docker-dev-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
# Least-privilege: GHCR push only. No id-token (no cosign) and no contents:write
|
|
# (no GitHub Release). Deliberately no `production` environment, so an
|
|
# integration image publishes automatically with no Docker Hub credentials ever
|
|
# in scope for this workflow.
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
|
|
jobs:
|
|
push_dev_image:
|
|
name: Build and push dev image to GHCR
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Check out the repo
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
|
with:
|
|
platforms: arm64
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
|
|
|
# Feeds the Dockerfile's APK_CACHE_BUST arg so the `apk upgrade` layer
|
|
# rebuilds at least once per calendar day even when every other input is
|
|
# cached, so the Trivy scan below sees a fresh layer. Same rationale as the
|
|
# release workflow; see the APK_CACHE_BUST arg in the Dockerfile.
|
|
- name: Compute daily apk cache bust value
|
|
id: apk-bust
|
|
run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
|
with:
|
|
registry: ghcr.io
|
|
# repository_owner is a fixed value (studio-saelix) on every event
|
|
# type; GITHUB_TOKEN is what actually authenticates the GHCR push.
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Extract metadata (tags, labels) for Docker
|
|
id: meta
|
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
|
with:
|
|
# Dedicated dev package, separate from the release package
|
|
# ghcr.io/studio-saelix/sencho. Never published to Docker Hub.
|
|
images: ghcr.io/studio-saelix/sencho-dev
|
|
# :dev moving pointer at the latest main build
|
|
# :dev-<sha> immutable per-commit tag for pinning a build on a host
|
|
# No latest / semver / moving-minor here: those are release-only and
|
|
# live in docker-publish.yml.
|
|
tags: |
|
|
type=raw,value=dev
|
|
type=sha,prefix=dev-,format=short
|
|
|
|
# Build an amd64-only variant into the local daemon first so Trivy scans
|
|
# the exact image before it is pushed, and the multi-arch push below reuses
|
|
# these layers from the buildkit daemon cache. Same gate the PR and release
|
|
# builds apply. Scanning only amd64 is a defensible proxy for the multi-arch
|
|
# manifest: distro package CVEs are arch-agnostic.
|
|
- name: Build dev image for pre-publish scan (amd64, loaded)
|
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
|
with:
|
|
context: .
|
|
push: false
|
|
load: true
|
|
platforms: linux/amd64
|
|
tags: localhost/sencho:dev-scan
|
|
cache-from: type=gha
|
|
build-args: |
|
|
APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }}
|
|
|
|
- name: Scan dev image for vulnerabilities (Trivy)
|
|
# Same HIGH/CRITICAL policy and OpenVEX source of truth (trivy.yaml ->
|
|
# security/vex/sencho.openvex.json) as PR and release CI. A failing scan
|
|
# blocks the `:dev` push, so a vulnerable integration image is never
|
|
# published for testing.
|
|
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
|
with:
|
|
image-ref: localhost/sencho:dev-scan
|
|
exit-code: '1'
|
|
severity: 'CRITICAL,HIGH'
|
|
format: 'table'
|
|
trivy-config: trivy.yaml
|
|
|
|
# Mirror of the release pre-publish smoke gate in docker-publish.yml:
|
|
# verify the source-built Docker CLI/Compose binaries exec, then start the
|
|
# container and poll /api/health until it returns 200. Runs BEFORE the push
|
|
# so a broken image never moves the `:dev` tag.
|
|
- name: Smoke test dev image (pre-publish)
|
|
run: |
|
|
set -euo pipefail
|
|
docker run --rm --entrypoint sh localhost/sencho:dev-scan -c \
|
|
'docker --version && docker compose version && git --version'
|
|
|
|
docker run -d --name sencho-dev-smoke -p 1852:1852 localhost/sencho:dev-scan
|
|
trap 'docker logs sencho-dev-smoke 2>&1 || true; docker rm -f sencho-dev-smoke >/dev/null 2>&1 || true' EXIT
|
|
for i in $(seq 1 30); do
|
|
if curl -fsS http://localhost:1852/api/health >/dev/null 2>&1; then
|
|
echo "Container healthy after ${i}s"
|
|
curl -s http://localhost:1852/api/health
|
|
exit 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
echo "FAILED: /api/health did not become ready within 30s"
|
|
exit 1
|
|
|
|
- name: Build and push dev image
|
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
|
with:
|
|
context: .
|
|
push: true
|
|
platforms: linux/amd64,linux/arm64
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
# gha cache, kept separate from the release buildcache
|
|
# (saelix/sencho:buildcache) so integration builds never pollute the
|
|
# cache that release publishes pull from.
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
build-args: |
|
|
APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }}
|