4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
351 lines
13 KiB
YAML
351 lines
13 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags: ['v*']
|
|
|
|
env:
|
|
NODE_VERSION: '22'
|
|
DOCKER_PLATFORMS: linux/amd64,linux/arm64
|
|
|
|
permissions:
|
|
contents: write
|
|
packages: write
|
|
|
|
jobs:
|
|
# ──── Release Gate (CI-strength) ──────────────────────────────────
|
|
# Mirror the CI workflow's gate jobs so a tag can never publish an image or GitHub Release that
|
|
# lint, the spec type-check, unit + e2e tests, the Postgres migration smoke, or the dashboard
|
|
# build/lint would have rejected. Each job below matches its CI counterpart; only the tag-match
|
|
# guard and the publish jobs (docker, release) are release-specific.
|
|
lint:
|
|
name: Lint
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
# Fail fast (before tests, image build, or the GitHub Release) if the release is inconsistent:
|
|
# the tag must match package.json, and the docs/CHANGELOG version guard must pass. This stops a
|
|
# mis-tagged or under-documented release from ever publishing an image or a GitHub Release.
|
|
- name: Verify tag matches package.json version
|
|
env:
|
|
TAG: ${{ github.ref_name }}
|
|
run: |
|
|
PKG_VERSION="$(node -p "require('./package.json').version")"
|
|
TAG_VERSION="${TAG#v}"
|
|
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
|
|
echo "::error::Tag '$TAG' (version '$TAG_VERSION') does not match package.json version '$PKG_VERSION'. Bump package.json or fix the tag before releasing."
|
|
exit 1
|
|
fi
|
|
echo "Tag $TAG matches package.json $PKG_VERSION"
|
|
|
|
- name: Security audit
|
|
run: npm audit --audit-level=critical
|
|
|
|
- name: Run ESLint
|
|
run: npm run lint
|
|
|
|
# Same gap as CI: `nest build` excludes specs, so a spec-only type regression slips past every
|
|
# existing release gate. tsconfig.json includes both src and test, so this is the full-program
|
|
# check that also covers spec files.
|
|
- name: Type-check full program (including specs)
|
|
run: npx tsc --noEmit -p tsconfig.json
|
|
|
|
- name: Check formatting
|
|
run: npm run format -- --check
|
|
|
|
- name: Check version consistency (docs track package.json)
|
|
run: npm run check:versions
|
|
|
|
test:
|
|
name: Test
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Run unit tests
|
|
run: npm test -- --coverage
|
|
|
|
- name: Run e2e smoke tests
|
|
run: npm run test:e2e
|
|
|
|
test-postgres:
|
|
name: Test (PostgreSQL migrations)
|
|
runs-on: ubuntu-latest
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
env:
|
|
POSTGRES_USER: openwa
|
|
POSTGRES_PASSWORD: openwa
|
|
POSTGRES_DB: openwa
|
|
ports:
|
|
- 5432:5432
|
|
options: >-
|
|
--health-cmd "pg_isready -U openwa"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Build (compiles the data migrations to dist/)
|
|
run: npm run build
|
|
|
|
# Applies the full data-migration chain to a real Postgres and asserts every generated-uuid PK
|
|
# has a DB DEFAULT — the dialect gap SQLite-only tests can't see.
|
|
- name: Migrate + uuid-default smoke against PostgreSQL
|
|
run: npm run test:pg-smoke
|
|
env:
|
|
DATABASE_TYPE: postgres
|
|
DATABASE_HOST: localhost
|
|
DATABASE_PORT: '5432'
|
|
DATABASE_USERNAME: openwa
|
|
DATABASE_PASSWORD: openwa
|
|
DATABASE_NAME: openwa
|
|
|
|
# Runtime-proves BuiltInFtsProvider on Postgres (websearch_to_tsquery + ts_headline against the
|
|
# STORED body_ts tsvector). The spec self-skips unless DATABASE_TYPE=postgres, so it is a no-op
|
|
# in the default test job and only executes here against the postgres:16 service.
|
|
- name: Postgres FTS provider spec
|
|
run: npx jest src/database/migrations/__tests__/1782400000000-AddMessagesFts.pg.spec.ts
|
|
env:
|
|
DATABASE_TYPE: postgres
|
|
DATABASE_HOST: localhost
|
|
DATABASE_PORT: '5432'
|
|
DATABASE_USERNAME: openwa
|
|
DATABASE_PASSWORD: openwa
|
|
DATABASE_NAME: openwa
|
|
|
|
dashboard:
|
|
name: Dashboard
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
cache-dependency-path: dashboard/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
run: cd dashboard && npm ci
|
|
|
|
- name: Run ESLint
|
|
run: cd dashboard && npm run lint
|
|
|
|
- name: Check i18n parity
|
|
run: cd dashboard && npm run i18n:check
|
|
|
|
- name: Build dashboard
|
|
run: cd dashboard && npm run build
|
|
|
|
- name: Run dashboard unit tests
|
|
run: cd dashboard && npm run test:unit
|
|
|
|
build:
|
|
name: Build
|
|
runs-on: ubuntu-latest
|
|
needs: [lint, test, dashboard]
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Build project
|
|
run: npm run build
|
|
|
|
# ──── Create GitHub Release ───────────────────────────────────────
|
|
release:
|
|
name: GitHub Release
|
|
runs-on: ubuntu-latest
|
|
# Gate the public Release on every CI-strength job AND a successfully built+pushed image that
|
|
# also boots, so a tag never produces a GitHub Release without passing the same gate CI enforces,
|
|
# a matching container image, and a runtime boot check on both architectures (boot-smoke runs the
|
|
# published image on amd64 + arm64). Trades a few minutes of buildx/boot latency for release
|
|
# safety.
|
|
needs: [lint, test, test-postgres, dashboard, build, docker, boot-smoke]
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Extract version from tag
|
|
id: version
|
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
|
|
|
- name: Extract release notes from CHANGELOG
|
|
id: changelog
|
|
run: |
|
|
# Extract the section for this version from CHANGELOG.md
|
|
VERSION="${{ steps.version.outputs.VERSION }}"
|
|
NOTES=$(awk "/^## \[${VERSION}\]/{flag=1; next} /^## \[/{flag=0} flag" CHANGELOG.md)
|
|
if [ -z "$NOTES" ]; then
|
|
NOTES="Release v${VERSION}"
|
|
fi
|
|
# Use EOF delimiter for multi-line output
|
|
echo "NOTES<<EOF" >> $GITHUB_OUTPUT
|
|
echo "$NOTES" >> $GITHUB_OUTPUT
|
|
echo "EOF" >> $GITHUB_OUTPUT
|
|
|
|
- name: Create GitHub Release
|
|
uses: softprops/action-gh-release@v3
|
|
with:
|
|
# Author the release as a real user (the PAT owner) instead of github-actions[bot].
|
|
# Add a repo secret RELEASE_PAT — a fine-grained PAT scoped to this repo with
|
|
# "Contents: read and write". Falls back to GITHUB_TOKEN (bot-authored) when the secret
|
|
# is absent, so the job never breaks before the token is configured.
|
|
token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
|
|
tag_name: ${{ github.ref_name }}
|
|
name: v${{ steps.version.outputs.VERSION }}
|
|
body: ${{ steps.changelog.outputs.NOTES }}
|
|
draft: false
|
|
prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }}
|
|
|
|
# ──── Docker Image with Version Tag ───────────────────────────────
|
|
docker:
|
|
name: Docker Release
|
|
runs-on: ubuntu-latest
|
|
needs: [lint, test, test-postgres, dashboard, build]
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@v4
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Extract metadata
|
|
id: meta
|
|
uses: docker/metadata-action@v6
|
|
with:
|
|
images: ghcr.io/${{ github.repository }}
|
|
tags: |
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
|
|
|
|
- name: Build and push
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
push: true
|
|
platforms: ${{ env.DOCKER_PLATFORMS }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
# provenance is generated by default (build-push-action v7); pin it explicitly and opt into
|
|
# an SBOM attestation so each release image carries an in-toto SLSA provenance + SBOM pair,
|
|
# verifiable via `docker buildx imagetools inspect`.
|
|
provenance: true
|
|
sbom: true
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
|
|
# ──── Boot Smoke (amd64 + arm64) ──────────────────────────────────
|
|
# The docker job proves the image BUILDS for both architectures; this proves the just-pushed image
|
|
# actually BOOTS and serves the liveness endpoint on both. A boot regression that only surfaces at
|
|
# runtime on one architecture (e.g. a native-dep/Chromium SIGTRAP on arm64 that a clean build still
|
|
# produces) cannot reach a public Release. Runs the published image — not a rebuild — against the
|
|
# dependency-free /api/health/live (NOT /ready, which needs a DB and would conflate boot failure
|
|
# with database setup). Both architectures are tried even if the first fails, so logs name both.
|
|
boot-smoke:
|
|
name: Boot smoke (amd64 + arm64)
|
|
runs-on: ubuntu-latest
|
|
needs: [docker]
|
|
steps:
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@v4
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Resolve published image ref
|
|
id: image
|
|
# metadata-action publishes the {{version}} tag (e.g. 0.8.17) from the v-prefixed git tag, so
|
|
# strip the leading "v" to match it. ${REPO,,} lowercases github.repository — GHCR normalizes
|
|
# the repo name to lowercase on push, and `docker run` rejects a mixed-case reference, so the
|
|
# pull target must be lowercase too (this repo is rmyndharis/OpenWA).
|
|
env:
|
|
REPO: ${{ github.repository }}
|
|
run: echo "ref=ghcr.io/${REPO,,}:${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Boot + liveness check on each architecture
|
|
env:
|
|
IMAGE: ${{ steps.image.outputs.ref }}
|
|
run: |
|
|
exit_code=0
|
|
for platform in linux/amd64 linux/arm64; do
|
|
echo "::group::boot-smoke $platform"
|
|
docker rm -f openwa-smoke >/dev/null 2>&1 || true
|
|
if ! docker run --rm --platform "$platform" -d -p 2785:2785 --name openwa-smoke "$IMAGE" >/dev/null; then
|
|
echo "FAIL: $platform — could not start container (image pull/run error)"
|
|
exit_code=1
|
|
echo "::endgroup::"
|
|
continue
|
|
fi
|
|
ok=""
|
|
for i in $(seq 1 40); do
|
|
if curl -sf http://127.0.0.1:2785/api/health/live >/dev/null 2>&1; then ok=1; break; fi
|
|
sleep 5
|
|
done
|
|
if [ -n "$ok" ]; then
|
|
echo "OK: $platform — /api/health/live returned 200"
|
|
else
|
|
echo "FAIL: $platform — /api/health/live did not return 200 within ~200s; container logs follow:"
|
|
docker logs openwa-smoke 2>&1 | tail -60 || true
|
|
exit_code=1
|
|
fi
|
|
docker stop openwa-smoke >/dev/null 2>&1 || true
|
|
docker rm -f openwa-smoke >/dev/null 2>&1 || true
|
|
echo "::endgroup::"
|
|
done
|
|
exit "$exit_code"
|