feat(release-chain): bygg→push→SBOM→cosign-sign→verify + signed-artifact-gate i cd.yml (kl0ud/st0re-mönstret) #59
@@ -23,7 +23,38 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
COOLIFY_APP_ID: ${{ vars.COOLIFY_APP_ID || 'TODO-no-coolify-app-provisioned-yet' }}
|
||||
IMAGE_REPO: git.cloud.siax.io/sax3l/0pt
|
||||
steps:
|
||||
# -----------------------------------------------------------------
|
||||
# SIGNED-ARTIFACT GATE (added 2026-09-14, same pattern as
|
||||
# sax3l/cl0ud + sax3l/st0re's fix/wire-signed-digest-to-deploy): once
|
||||
# the guard step below stops failing (i.e. once a real Coolify app
|
||||
# exists for 0pt and COOLIFY_APP_ID is filled in), this job will
|
||||
# REFUSE to call Coolify at all unless a cosign-verified image exists
|
||||
# in the registry for the EXACT commit being deployed.
|
||||
# `.gitea/workflows/release.yml` (added in this same change) is the
|
||||
# piece that builds, SBOMs, signs, and pushes that image.
|
||||
#
|
||||
# HONEST LIMITATION (identical to cl0ud/st0re/h0st's writeup):
|
||||
# `siax.config.json`'s `deploy.target: "coolify"` and this file's own
|
||||
# `/api/v1/deploy` call assume a `dockerfile` build-pack Application
|
||||
# once provisioned -- same as cl0ud/st0re/h0st, Coolify would clone
|
||||
# this repo and run its OWN independent `docker build`, never pulling
|
||||
# the image release.yml pushed and signed, and `/api/v1/deploy` has no
|
||||
# digest parameter. True digest-pinning would need 0pt's eventual
|
||||
# Coolify app migrated to the separate "Docker Image" resource type --
|
||||
# a production infrastructure decision that hasn't been made yet
|
||||
# (0pt isn't even provisioned on Coolify at all as of this writing;
|
||||
# see the guard step below and docs/runbook/deploy.md §1), not
|
||||
# something this CI-YAML change can or should decide.
|
||||
#
|
||||
# So: what this DOES guarantee, from the day 0pt's Coolify app exists
|
||||
# -- Coolify is only ever told to rebuild+redeploy a commit whose
|
||||
# release-chain artifact was built, SBOM'd, signed, and had its
|
||||
# signature RE-verified in THIS job. What it does NOT guarantee --
|
||||
# bit-identical bytes between the signed image and whatever Coolify's
|
||||
# own rebuild produces from the same source.
|
||||
# -----------------------------------------------------------------
|
||||
- name: Guard against unset token / placeholder app id
|
||||
run: |
|
||||
if [ -z "${{ secrets.COOLIFY_TOKEN }}" ]; then
|
||||
@@ -34,10 +65,91 @@ jobs:
|
||||
echo "::error::No Coolify app has been provisioned for 0pt yet. Create the app in the Coolify UI (cloud.siax.io), copy its UUID into COOLIFY_APP_ID in this file, and set the COOLIFY_TOKEN repo secret before this workflow can deploy anything."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve this commit's release-chain tag (same scheme as release.yml)
|
||||
id: sha
|
||||
run: |
|
||||
set -euo pipefail
|
||||
short="$(git rev-parse --short=12 HEAD)"
|
||||
echo "short=$short" >> "$GITHUB_OUTPUT"
|
||||
echo "image_ref=${IMAGE_REPO}:sha-$short" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Docker login (git.cloud.siax.io) — read access to check release.yml's output
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
registry_host="$(echo "$IMAGE_REPO" | cut -d/ -f1)"
|
||||
echo "$REGISTRY_TOKEN" | docker login "$registry_host" -u "$REGISTRY_USER" --password-stdin
|
||||
|
||||
- name: Install cosign v3.1.3 (pinned, checksum-verified — never :latest)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd /tmp
|
||||
curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign-linux-amd64
|
||||
curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign_checksums.txt
|
||||
grep 'cosign-linux-amd64$' cosign_checksums.txt | sha256sum -c -
|
||||
install -m 0755 cosign-linux-amd64 /usr/local/bin/cosign
|
||||
cosign version
|
||||
|
||||
- name: Wait for release.yml's signed artifact for THIS commit, then verify (fail closed)
|
||||
id: verify
|
||||
env:
|
||||
IMAGE_REF: ${{ steps.sha.outputs.image_ref }}
|
||||
SHORT: ${{ steps.sha.outputs.short }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f cosign.pub || { echo "::error::cosign.pub not found at repo root — cannot verify, refusing to deploy"; exit 1; }
|
||||
|
||||
# release.yml runs as a SEPARATE workflow triggered by the same push
|
||||
# (Gitea/GitHub Actions has no cross-workflow-file `needs:`), so this
|
||||
# job cannot depend on that job directly — it polls the registry for
|
||||
# the artifact release.yml produces instead. Bounded: a genuinely
|
||||
# broken/skipped release run fails this job loudly rather than
|
||||
# hanging forever.
|
||||
attempt=0
|
||||
max_attempts=40
|
||||
until docker pull "$IMAGE_REF" >/tmp/pull.log 2>&1; do
|
||||
attempt=$((attempt + 1))
|
||||
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||
echo "::error::no signed image found at $IMAGE_REF after $max_attempts attempts (10 min) — release.yml has not produced an artifact for sha-$SHORT. Refusing to deploy an unverified rebuild."
|
||||
cat /tmp/pull.log
|
||||
exit 1
|
||||
fi
|
||||
echo "image not yet available at $IMAGE_REF (attempt $attempt/$max_attempts) — waiting for release.yml..."
|
||||
sleep 15
|
||||
done
|
||||
|
||||
# Read back the digest the registry actually stored for this tag —
|
||||
# same filter release.yml itself uses (RepoDigests can hold multiple
|
||||
# entries; never trust index 0) — and verify THAT digest, never the
|
||||
# mutable tag alone, so a tag reassigned after signing can't slip past.
|
||||
repo_digest="$(docker inspect --format='{{json .RepoDigests}}' "$IMAGE_REF" \
|
||||
| node -e "const rd=JSON.parse(require('fs').readFileSync(0,'utf8')); const prefix=process.env.IMAGE_REPO+'@'; const m=rd.find(r=>r.startsWith(prefix)); if(!m){console.error('no RepoDigests entry starting with '+prefix+' among: '+JSON.stringify(rd));process.exit(1);} console.log(m);")"
|
||||
digest="${repo_digest##*@}"
|
||||
echo "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo "::error::unexpected digest shape: $digest"; exit 1; }
|
||||
|
||||
echo "verifying cosign signature on ${IMAGE_REPO}@${digest} ..."
|
||||
cosign verify --key cosign.pub "${IMAGE_REPO}@${digest}"
|
||||
|
||||
echo "digest=$digest" >> "$GITHUB_OUTPUT"
|
||||
echo "SIGNED ARTIFACT VERIFIED: ${IMAGE_REPO}@${digest} — authorizing Coolify redeploy of sha-$SHORT"
|
||||
|
||||
- name: Trigger Coolify redeploy
|
||||
env:
|
||||
COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }}
|
||||
VERIFIED_DIGEST: ${{ steps.verify.outputs.digest }}
|
||||
run: |
|
||||
# See the SIGNED-ARTIFACT GATE comment above: Coolify's /deploy has
|
||||
# no digest/image param to pass, so $VERIFIED_DIGEST is not (and
|
||||
# cannot be) sent to Coolify — it is recorded here as the
|
||||
# provenance proof that authorized this specific redeploy call.
|
||||
echo "authorized by verified digest: $VERIFIED_DIGEST"
|
||||
curl -fsS -X POST \
|
||||
"https://cloud.siax.io/api/v1/deploy?uuid=${COOLIFY_APP_ID}&force=false" \
|
||||
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
name: Release chain (P007 pattern)
|
||||
|
||||
# ADDED 2026-09-14: 0pt had NO SBOM/sign/digest chain at all before this file
|
||||
# (re-confirmed live: no cosign/syft/sbom hit anywhere under .gitea/workflows/,
|
||||
# only doc mentions in CHANGELOG.md/docs/architecture/ARCHITECTURE.md). This
|
||||
# is the proven sax3l/cl0ud + sax3l/st0re pattern (originally inlined
|
||||
# 2026-09-10 from sax3l/siax-standard's release-chain.yml, then hardened
|
||||
# 2026-09-14), authored directly for 0pt because cross-repo `uses:` clones
|
||||
# anonymously on this Gitea instance and fails with "repository not found"
|
||||
# against private repos (the same documented trap that forced cl0ud/st0re to
|
||||
# inline rather than call out to a shared workflow).
|
||||
#
|
||||
# Cosign keypair is 0pt-specific (generated offline 2026-09-14, never
|
||||
# transmitted anywhere); cosign.pub in the repo root is the corresponding
|
||||
# public key. Neither this Dockerfile nor h0st's declares
|
||||
# `ARG GIT_SHA`/`ARG BUILD_TIME`, so this file does not pass them as
|
||||
# --build-arg (would just produce a harmless-but-noisy "not consumed"
|
||||
# warning) -- GIT_SHA/DIGEST still go into the evidence JSON below either way.
|
||||
#
|
||||
# STATUS NOTE (see .gitea/workflows/cd.yml's own header): 0pt has NO Coolify
|
||||
# Application provisioned yet -- cd.yml's guard step already fails closed on
|
||||
# the placeholder COOLIFY_APP_ID for that reason, independent of this file.
|
||||
# This release chain is deliberately built ahead of that: once the app
|
||||
# exists and cd.yml's placeholder is replaced with a real UUID, the signed-
|
||||
# artifact gate added to cd.yml in this same change is already wired and
|
||||
# ready, instead of being a second follow-up piece of work.
|
||||
#
|
||||
# Krävda repo-secrets (NOT YET SET on sax3l/0pt as of this writing -- this
|
||||
# file will fail closed on its first run until Simon sets them, same
|
||||
# prerequisite cl0ud/st0re needed before their release.yml went green
|
||||
# 2026-09-10): REGISTRY_USER/REGISTRY_TOKEN (git.cloud.siax.io push),
|
||||
# COSIGN_PRIVATE_KEY/COSIGN_PASSWORD (the private half of this repo's
|
||||
# cosign.pub), SIAX_CI_TOKEN (scoped write:repository token for the
|
||||
# evidence-commit push -- the runner's ephemeral actions token gets a
|
||||
# pre-receive 500, GetUserByID(uid=-2), same fleet-wide gotcha as cl0ud/st0re).
|
||||
# Verification: cosign verify --key cosign.pub git.cloud.siax.io/sax3l/0pt@<digest>
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build, push, SBOM, sign, record evidence
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
IMAGE_REPO: git.cloud.siax.io/sax3l/0pt
|
||||
DOCKERFILE: Dockerfile
|
||||
EVIDENCE_DIR: siax
|
||||
SERVICE_NAME: "@siax/0pt"
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
RUN_ATTEMPT: ${{ github.run_attempt }}
|
||||
steps:
|
||||
- uses: https://git.siax.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4, mirrored + SHA-pinned (SUP-06, same pin n0d/cl0ud/st0re already use)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve SHA + tag (never :latest)
|
||||
id: sha
|
||||
run: |
|
||||
set -euo pipefail
|
||||
full="$(git rev-parse HEAD)"
|
||||
short="$(git rev-parse --short=12 HEAD)"
|
||||
built_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "full=$full" >> "$GITHUB_OUTPUT"
|
||||
echo "short=$short" >> "$GITHUB_OUTPUT"
|
||||
echo "built_at=$built_at" >> "$GITHUB_OUTPUT"
|
||||
echo "image_ref=${IMAGE_REPO}:sha-$short" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Docker login (git.cloud.siax.io)
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
registry_host="$(echo "$IMAGE_REPO" | cut -d/ -f1)"
|
||||
echo "$REGISTRY_TOKEN" | docker login "$registry_host" -u "$REGISTRY_USER" --password-stdin
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
IMAGE_REF: ${{ steps.sha.outputs.image_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker build \
|
||||
-f "$DOCKERFILE" \
|
||||
-t "$IMAGE_REF" \
|
||||
.
|
||||
|
||||
- name: Push (tag sha-<12>, never :latest)
|
||||
env:
|
||||
IMAGE_REF: ${{ steps.sha.outputs.image_ref }}
|
||||
run: docker push "$IMAGE_REF"
|
||||
|
||||
- name: Read back the digest the registry actually stored (never assumed)
|
||||
id: digest
|
||||
env:
|
||||
IMAGE_REF: ${{ steps.sha.outputs.image_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# `docker inspect`'s RepoDigests can carry MULTIPLE entries (e.g. one for
|
||||
# whatever this image was originally pulled/built FROM, plus one per registry
|
||||
# it has since been pushed to) — index 0 is not reliably "the one we just
|
||||
# pushed". Filter for the entry whose repo matches IMAGE_REPO instead (same
|
||||
# filter proven live on cl0ud's P007 build-sandbox: a probe push showed
|
||||
# RepoDigests holding both an "alpine@sha256:..." entry AND the
|
||||
# "git.cloud.siax.io/...@sha256:..." entry side by side).
|
||||
repo_digest="$(docker inspect --format='{{json .RepoDigests}}' "$IMAGE_REF" \
|
||||
| node -e "const rd=JSON.parse(require('fs').readFileSync(0,'utf8')); const prefix=process.env.IMAGE_REPO+'@'; const match=rd.find(r=>r.startsWith(prefix)); if(!match){console.error('no RepoDigests entry starting with '+prefix+' among: '+JSON.stringify(rd));process.exit(1);} console.log(match);")"
|
||||
digest="${repo_digest##*@}"
|
||||
echo "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo "::error::unexpected digest shape: $digest"; exit 1; }
|
||||
echo "digest=$digest" >> "$GITHUB_OUTPUT"
|
||||
echo "pushed and verified: $IMAGE_REF @ $digest"
|
||||
|
||||
# KNOWN RISK, NOT YET FIXED (same open issue as cl0ud's/st0re's/h0st's
|
||||
# copy of this file, flagged 2026-09-03/09-14): this fleet's runner
|
||||
# egress is confirmed to reach raw.githubusercontent.com but NOT
|
||||
# github.com/*/releases/download URLs (curl -sfL -> silent failure).
|
||||
# The two `curl -fsSL -O https://github.com/.../releases/download/...`
|
||||
# steps below (syft, cosign) use exactly that broken URL shape, and
|
||||
# neither tool ships a raw.githubusercontent.com-hosted installer that
|
||||
# avoids a github.com/releases fetch for the actual binary. Left as
|
||||
# pinned, checksum-verified curl installs, same as cl0ud/st0re/h0st;
|
||||
# the first real run of this file on 0pt's own runners is the first
|
||||
# live evidence for OR against this risk actually manifesting here
|
||||
# (not simulated in this dry-run pass — see PR description).
|
||||
- name: Install syft v1.51.1 (pinned, checksum-verified — never :latest)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd /tmp
|
||||
curl -fsSL -O https://github.com/anchore/syft/releases/download/v1.51.1/syft_1.51.1_linux_amd64.tar.gz
|
||||
curl -fsSL -O https://github.com/anchore/syft/releases/download/v1.51.1/syft_1.51.1_checksums.txt
|
||||
grep 'syft_1.51.1_linux_amd64.tar.gz' syft_1.51.1_checksums.txt | sha256sum -c -
|
||||
tar -xzf syft_1.51.1_linux_amd64.tar.gz syft
|
||||
install -m 0755 syft /usr/local/bin/syft
|
||||
syft version
|
||||
|
||||
- name: Install cosign v3.1.3 (pinned, checksum-verified — never :latest)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd /tmp
|
||||
curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign-linux-amd64
|
||||
curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign_checksums.txt
|
||||
grep 'cosign-linux-amd64$' cosign_checksums.txt | sha256sum -c -
|
||||
install -m 0755 cosign-linux-amd64 /usr/local/bin/cosign
|
||||
cosign version
|
||||
|
||||
- name: SBOM (syft, spdx-json, against the DIGEST not the tag)
|
||||
env:
|
||||
DIGEST: ${{ steps.digest.outputs.digest }}
|
||||
SHORT: ${{ steps.sha.outputs.short }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$EVIDENCE_DIR/_release-artifacts"
|
||||
syft "${IMAGE_REPO}@${DIGEST}" -o "spdx-json=${EVIDENCE_DIR}/_release-artifacts/sbom-${SHORT}.spdx.json"
|
||||
|
||||
- name: Sign (cosign, key-based — see cosign.pub at repo root)
|
||||
env:
|
||||
COSIGN_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
DIGEST: ${{ steps.digest.outputs.digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "$COSIGN_KEY" > /tmp/cosign.key
|
||||
cosign sign --yes --key /tmp/cosign.key "${IMAGE_REPO}@${DIGEST}"
|
||||
rm -f /tmp/cosign.key
|
||||
|
||||
- name: Verify the signature just written (fail closed if it does not verify)
|
||||
env:
|
||||
DIGEST: ${{ steps.digest.outputs.digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f cosign.pub || { echo "::error::cosign.pub not found at repo root — commit the PUBLIC key (not a secret) so anyone can verify without CI access"; exit 1; }
|
||||
cosign verify --key cosign.pub "${IMAGE_REPO}@${DIGEST}"
|
||||
|
||||
- name: Write siax.source-bundle + siax.production-evidence records
|
||||
env:
|
||||
SHA_FULL: ${{ steps.sha.outputs.full }}
|
||||
SHA_SHORT: ${{ steps.sha.outputs.short }}
|
||||
DIGEST: ${{ steps.digest.outputs.digest }}
|
||||
IMAGE_REF: ${{ steps.sha.outputs.image_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$EVIDENCE_DIR/source-bundles" "$EVIDENCE_DIR/production-evidence"
|
||||
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
repo_name="$(basename "$(git rev-parse --show-toplevel)")"
|
||||
|
||||
cat > "$EVIDENCE_DIR/source-bundles/release-${SHA_SHORT}.json" <<JSON
|
||||
{
|
||||
"id": "sb-release-${SHA_SHORT}",
|
||||
"repo": "$repo_name",
|
||||
"commitSha": "$SHA_FULL",
|
||||
"branch": "$REF_NAME",
|
||||
"buildId": "${RUN_ID}-${RUN_ATTEMPT}",
|
||||
"artifactId": "$IMAGE_REF",
|
||||
"producedAt": "$now"
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$EVIDENCE_DIR/production-evidence/release-${SHA_SHORT}-artifact.json" <<JSON
|
||||
{
|
||||
"id": "pe-artifact-${SHA_SHORT}",
|
||||
"repo": "$repo_name",
|
||||
"evidenceType": "artifact-digest",
|
||||
"verifiedAt": "$now",
|
||||
"verifiedBy": "gitea-ci",
|
||||
"result": "pass",
|
||||
"proofRef": "pushed $IMAGE_REF to $IMAGE_REPO",
|
||||
"tool": "docker",
|
||||
"sourceSha": "$SHA_FULL",
|
||||
"digest": "$DIGEST"
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$EVIDENCE_DIR/production-evidence/release-${SHA_SHORT}-sbom.json" <<JSON
|
||||
{
|
||||
"id": "pe-sbom-${SHA_SHORT}",
|
||||
"repo": "$repo_name",
|
||||
"evidenceType": "sbom",
|
||||
"verifiedAt": "$now",
|
||||
"verifiedBy": "gitea-ci",
|
||||
"result": "pass",
|
||||
"proofRef": "$EVIDENCE_DIR/_release-artifacts/sbom-${SHA_SHORT}.spdx.json",
|
||||
"tool": "syft",
|
||||
"sbomFormat": "spdx-json",
|
||||
"digest": "$DIGEST"
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$EVIDENCE_DIR/production-evidence/release-${SHA_SHORT}-signature.json" <<JSON
|
||||
{
|
||||
"id": "pe-signature-${SHA_SHORT}",
|
||||
"repo": "$repo_name",
|
||||
"evidenceType": "signature",
|
||||
"verifiedAt": "$now",
|
||||
"verifiedBy": "gitea-ci",
|
||||
"result": "pass",
|
||||
"proofRef": "cosign verify --key cosign.pub ${IMAGE_REPO}@${DIGEST} (verified in this same run)",
|
||||
"tool": "cosign",
|
||||
"signatureTool": "cosign",
|
||||
"keyless": false,
|
||||
"digest": "$DIGEST"
|
||||
}
|
||||
JSON
|
||||
|
||||
# upload-artifact deliberately OMITTED: act_runner on this fleet has no
|
||||
# GHES-artifact-API, so upload-artifact@v4 always dies even with
|
||||
# continue-on-error: true and can abort the whole job before the
|
||||
# evidence-commit step below runs (proven on cl0ud run 23340, fixed
|
||||
# there 2026-09-14 by deleting the step outright — applied here from
|
||||
# the start instead of repeating the same bug). Evidence lands in the
|
||||
# repo via the commit step below regardless.
|
||||
- name: Commit evidence back to the calling repo
|
||||
if: true
|
||||
env:
|
||||
SHA_SHORT: ${{ steps.sha.outputs.short }}
|
||||
SIAX_CI_TOKEN: ${{ secrets.SIAX_CI_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "siax-ci"
|
||||
git config user.email "ci@siax.io"
|
||||
git add "$EVIDENCE_DIR/source-bundles" "$EVIDENCE_DIR/production-evidence"
|
||||
if git diff --cached --quiet; then
|
||||
echo "no evidence changes to commit"
|
||||
else
|
||||
git commit -m "chore(release-chain): evidence for sha-${SHA_SHORT} [skip ci]"
|
||||
# Runnerns ephemeral actions-token (uid -2) kan inte pusha:
|
||||
# pre-receive GetUserByID(-2) → 500. Autentisera som riktig
|
||||
# Gitea-user via SIAX_CI_TOKEN (samma mönster som cl0ud/st0re/h0st).
|
||||
git config http.https://git.siax.io/.extraheader "AUTHORIZATION: basic $(printf 'x-access-token:%s' "${SIAX_CI_TOKEN}" | base64 -w0)"
|
||||
git push origin "HEAD:${REF_NAME}"
|
||||
fi
|
||||
@@ -0,0 +1,4 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3EIrsPWBhSnH6HL7yD9US1RtXDg4
|
||||
kHaLp8mDefBhepQRFsFPedyWwNaaqEwA3Pe1G2qBdm/FgAMAFkYjZyvgCg==
|
||||
-----END PUBLIC KEY-----
|
||||
+27
-1
@@ -14,10 +14,36 @@
|
||||
(se backup/restore + incident runbooks).
|
||||
- [ ] DNS-cutover planerad: `0pt.app` → Coolify-appens ingress (eller Caddy).
|
||||
|
||||
## 1b. Signerad-artefakt-grind (tillagd 2026-09-14, samma mönster som cl0ud/st0re/h0st)
|
||||
|
||||
`.gitea/workflows/release.yml` (nytt) bygger, SBOM:ar (syft), signerar
|
||||
(cosign) och evidensbokför varje push till `main`, och pushar till
|
||||
`git.cloud.siax.io/sax3l/0pt:sha-<12>`. `cd.yml`s `deploy`-jobb har nu ett
|
||||
extra steg (efter guard-steget i punkt 1 ovan) som kräver att exakt denna
|
||||
commits avbildning finns i registret och att dess cosign-signatur
|
||||
verifierar (mot committad `cosign.pub`) innan Coolify överhuvudtaget
|
||||
anropas.
|
||||
|
||||
Detta är byggt FÖRE Coolify-appen finns (se blockerarna i punkt 1) — så att
|
||||
grinden redan är på plats den dag appen provisioneras, i stället för att bli
|
||||
ett separat uppföljningsarbete. Samma ärliga begränsning som cl0ud/st0re/
|
||||
h0st: när appen väl provisioneras som `dockerfile` build pack (så som
|
||||
`siax.config.json` föreskriver) kör Coolify sin EGEN oberoende
|
||||
`docker build` från git-källan, inte den signerade avbildningen —
|
||||
`/api/v1/deploy` saknar digest-parameter helt. Sann digest-pinning skulle
|
||||
kräva att appen provisioneras som Coolifys separata `dockerimage`-resurstyp
|
||||
i stället, ett beslut som inte är fattat än. Se `.gitea/workflows/cd.yml`s
|
||||
"SIGNED-ARTIFACT GATE"-kommentar för den fullständiga skrivningen.
|
||||
|
||||
**Inte satt på `sax3l/0pt` ännu:** repo-secrets `REGISTRY_USER`,
|
||||
`REGISTRY_TOKEN`, `COSIGN_PRIVATE_KEY`, `COSIGN_PASSWORD`, `SIAX_CI_TOKEN` —
|
||||
`release.yml` fail-closed:ar på sin första körning tills dessa finns.
|
||||
|
||||
## 2. Deploy (när appen finns)
|
||||
|
||||
1. Push till `main` → `.gitea/workflows/cd.yml` POSTar till
|
||||
`https://cloud.siax.io/api/v1/deploy?uuid=$COOLIFY_APP_ID&force=false`.
|
||||
`https://cloud.siax.io/api/v1/deploy?uuid=$COOLIFY_APP_ID&force=false`,
|
||||
efter att den signerade-artefakt-grinden i punkt 1b gått grön.
|
||||
2. Följ run i Gitea Actions; verifiera att den slutar grön.
|
||||
3. Verifiera hälsa: `curl https://0pt.app/api/health` → `ok:true` och
|
||||
`db: configured` (när Postgres kopplats in).
|
||||
|
||||
Reference in New Issue
Block a user