Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b7f771c6 | ||
|
|
9e2e536a67 | ||
|
|
0762627a76 | ||
|
|
29e5de1082 | ||
|
|
315a3fe6a5 |
@@ -1,24 +1,8 @@
|
||||
name: Deploy to Production
|
||||
|
||||
name: deploy (moved to native pipeline)
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: hanzo-deploy-linux-amd64
|
||||
|
||||
notice:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Push to release branch
|
||||
run: |
|
||||
git checkout release || git checkout -b release
|
||||
git merge --ff-only main
|
||||
git push origin release
|
||||
- run: echo "native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror"
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
name: Docker
|
||||
name: Docker (moved to native pipeline)
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, dev, test]
|
||||
tags: ['v*']
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
jobs:
|
||||
docker:
|
||||
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
|
||||
with:
|
||||
image: ghcr.io/hanzoai/esign
|
||||
dockerfile: docker/Dockerfile
|
||||
# arm64 builds run on canonical native arcd labels — matches
|
||||
# spark (self-hosted + linux + arm64). Org isolation comes from
|
||||
# GitHub runner group + arcd's repo allowlist.
|
||||
runner-arm64: self-hosted
|
||||
secrets: inherit
|
||||
notice:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror"
|
||||
|
||||
@@ -1,182 +1,8 @@
|
||||
name: Publish Docker
|
||||
|
||||
name: Publish Docker (moved to native pipeline)
|
||||
on:
|
||||
push:
|
||||
branches: ['release']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Git tag to build and publish (e.g., v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build_and_publish_platform_containers:
|
||||
name: Build and publish platform containers
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- warp-ubuntu-latest-x64-4x
|
||||
- warp-ubuntu-latest-arm64-4x
|
||||
|
||||
notice:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
fetch-tags: true
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build the docker image
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
NEXT_PRIVATE_TELEMETRY_KEY: ${{ secrets.NEXT_PRIVATE_TELEMETRY_KEY }}
|
||||
NEXT_PRIVATE_TELEMETRY_HOST: ${{ secrets.NEXT_PRIVATE_TELEMETRY_HOST }}
|
||||
APP_VERSION: ${{ inputs.tag || '' }}
|
||||
run: |
|
||||
if [ -z "$APP_VERSION" ]; then
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
fi
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker build \
|
||||
-f ./docker/Dockerfile \
|
||||
--progress=plain \
|
||||
--build-arg NEXT_PRIVATE_TELEMETRY_KEY="${NEXT_PRIVATE_TELEMETRY_KEY:-}" \
|
||||
--build-arg NEXT_PRIVATE_TELEMETRY_HOST="${NEXT_PRIVATE_TELEMETRY_HOST:-}" \
|
||||
-t "hanzo-sign/hanzo-sign-$BUILD_PLATFORM:latest" \
|
||||
-t "hanzo-sign/hanzo-sign-$BUILD_PLATFORM:$GIT_SHA" \
|
||||
-t "hanzo-sign/hanzo-sign-$BUILD_PLATFORM:$APP_VERSION" \
|
||||
-t "ghcr.io/hanzo-sign/hanzo-sign-$BUILD_PLATFORM:latest" \
|
||||
-t "ghcr.io/hanzo-sign/hanzo-sign-$BUILD_PLATFORM:$GIT_SHA" \
|
||||
-t "ghcr.io/hanzo-sign/hanzo-sign-$BUILD_PLATFORM:$APP_VERSION" \
|
||||
.
|
||||
|
||||
- name: Push the docker image to DockerHub
|
||||
run: docker push --all-tags "hanzo-sign/hanzo-sign-$BUILD_PLATFORM"
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
|
||||
- name: Push the docker image to GitHub Container Registry
|
||||
run: docker push --all-tags "ghcr.io/hanzo-sign/hanzo-sign-$BUILD_PLATFORM"
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
|
||||
create_and_publish_manifest:
|
||||
name: Create and publish manifest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
needs: build_and_publish_platform_containers
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
fetch-tags: true
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Create and push DockerHub manifest
|
||||
env:
|
||||
APP_VERSION: ${{ inputs.tag || '' }}
|
||||
run: |
|
||||
if [ -z "$APP_VERSION" ]; then
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
fi
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
hanzo-sign/hanzo-sign:latest \
|
||||
--amend hanzo-sign/hanzo-sign-amd64:latest \
|
||||
--amend hanzo-sign/hanzo-sign-arm64:latest
|
||||
|
||||
docker manifest push hanzo-sign/hanzo-sign:latest
|
||||
fi
|
||||
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
hanzo-sign/hanzo-sign:rc \
|
||||
--amend hanzo-sign/hanzo-sign-amd64:rc \
|
||||
--amend hanzo-sign/hanzo-sign-arm64:rc
|
||||
|
||||
docker manifest push hanzo-sign/hanzo-sign:rc
|
||||
fi
|
||||
|
||||
docker manifest create \
|
||||
hanzo-sign/hanzo-sign:$GIT_SHA \
|
||||
--amend hanzo-sign/hanzo-sign-amd64:$GIT_SHA \
|
||||
--amend hanzo-sign/hanzo-sign-arm64:$GIT_SHA
|
||||
|
||||
docker manifest create \
|
||||
hanzo-sign/hanzo-sign:$APP_VERSION \
|
||||
--amend hanzo-sign/hanzo-sign-amd64:$APP_VERSION \
|
||||
--amend hanzo-sign/hanzo-sign-arm64:$APP_VERSION
|
||||
|
||||
docker manifest push hanzo-sign/hanzo-sign:$GIT_SHA
|
||||
docker manifest push hanzo-sign/hanzo-sign:$APP_VERSION
|
||||
|
||||
- name: Create and push Github Container Registry manifest
|
||||
env:
|
||||
APP_VERSION: ${{ inputs.tag || '' }}
|
||||
run: |
|
||||
if [ -z "$APP_VERSION" ]; then
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
fi
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
ghcr.io/hanzo-sign/hanzo-sign:latest \
|
||||
--amend ghcr.io/hanzo-sign/hanzo-sign-amd64:latest \
|
||||
--amend ghcr.io/hanzo-sign/hanzo-sign-arm64:latest
|
||||
|
||||
docker manifest push ghcr.io/hanzo-sign/hanzo-sign:latest
|
||||
fi
|
||||
|
||||
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
docker manifest create \
|
||||
ghcr.io/hanzo-sign/hanzo-sign:rc \
|
||||
--amend ghcr.io/hanzo-sign/hanzo-sign-amd64:rc \
|
||||
--amend ghcr.io/hanzo-sign/hanzo-sign-arm64:rc
|
||||
|
||||
docker manifest push ghcr.io/hanzo-sign/hanzo-sign:rc
|
||||
fi
|
||||
|
||||
docker manifest create \
|
||||
ghcr.io/hanzo-sign/hanzo-sign:$GIT_SHA \
|
||||
--amend ghcr.io/hanzo-sign/hanzo-sign-amd64:$GIT_SHA \
|
||||
--amend ghcr.io/hanzo-sign/hanzo-sign-arm64:$GIT_SHA
|
||||
|
||||
docker manifest create \
|
||||
ghcr.io/hanzo-sign/hanzo-sign:$APP_VERSION \
|
||||
--amend ghcr.io/hanzo-sign/hanzo-sign-amd64:$APP_VERSION \
|
||||
--amend ghcr.io/hanzo-sign/hanzo-sign-arm64:$APP_VERSION
|
||||
|
||||
docker manifest push ghcr.io/hanzo-sign/hanzo-sign:$GIT_SHA
|
||||
docker manifest push ghcr.io/hanzo-sign/hanzo-sign:$APP_VERSION
|
||||
- run: echo "native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: deploy
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: hanzo-linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build + push image
|
||||
run: |
|
||||
SHA="${GITHUB_SHA::8}"
|
||||
buildctl-daemonless.sh build --frontend=dockerfile.v0 \
|
||||
--opt context="${{ github.server_url }}/${{ github.repository }}.git#${GITHUB_SHA}" \
|
||||
--opt filename=docker/Dockerfile --opt platform=linux/amd64 \
|
||||
--secret id=GIT_AUTH_TOKEN,env=GIT_AUTH_TOKEN \
|
||||
--output "type=image,name=ghcr.io/hanzoai/sign:${SHA},push=true" --progress=plain
|
||||
env:
|
||||
GIT_AUTH_TOKEN: ${{ secrets.GIT_CLONE_TOKEN }}
|
||||
- name: Deploy — declare tag to operator
|
||||
run: |
|
||||
for app in sign; do
|
||||
kubectl -n hanzo patch app "$app" --type=merge -p "{\"spec\":{\"image\":{\"repository\":\"ghcr.io/hanzoai/sign\",\"tag\":\"${GITHUB_SHA::8}\"}}}"
|
||||
done
|
||||
+35
-5
@@ -106,6 +106,22 @@
|
||||
}
|
||||
function signatureForField(fieldId) { return firstRow(query('SELECT * FROM signatures WHERE field_id = ?', [fieldId])); }
|
||||
|
||||
// assertTurn enforces SEQUENTIAL signing order: a recipient may only act once
|
||||
// EVERY signer/approver ahead of them (lower signing_order, then created_at — the
|
||||
// listRecipients order) has SIGNED. PARALLEL documents impose no order. Called by
|
||||
// signField and signComplete so a later party cannot sign before earlier ones.
|
||||
function assertTurn(doc, r) {
|
||||
if (doc.signing_order !== 'SEQUENTIAL') return;
|
||||
var recipients = listRecipients(doc.id); // ORDER BY signing_order ASC, created_at ASC
|
||||
for (var i = 0; i < recipients.length; i++) {
|
||||
var o = recipients[i];
|
||||
if (o.id === r.id) return; // reached this recipient — everyone ahead has signed
|
||||
if (SIGNING_ROLES[o.role] && o.signing_status !== SIGNING.SIGNED) {
|
||||
throw new HttpError(403, 'waiting for an earlier signer (sequential signing order)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function documentView(doc) {
|
||||
var recipients = listRecipients(doc.id).map(function (r) {
|
||||
return {
|
||||
@@ -143,8 +159,15 @@
|
||||
var t = now();
|
||||
|
||||
var dataId = id();
|
||||
// PDF bytes live on the object-storage seam (__blob), NOT inline in the
|
||||
// per-tenant SQLite — a 32 MiB base64 PDF in a TEXT column would bloat the
|
||||
// tenant DB and be re-copied on every read. document_data.data holds the opaque
|
||||
// blob KEY; initial_data keeps the ORIGINAL key so the pre-seal PDF is never
|
||||
// overwritten. __blob is tenant-scoped by the host, so the key is local.
|
||||
var initialKey = 'doc/' + dataId + '/initial';
|
||||
__blob.put(initialKey, pdf);
|
||||
exec('INSERT INTO document_data (id, type, data, initial_data) VALUES (?,?,?,?)',
|
||||
[dataId, 'BYTES_64', pdf, pdf]);
|
||||
[dataId, 'BLOB_KEY', initialKey, initialKey]);
|
||||
|
||||
var docId = id();
|
||||
exec(
|
||||
@@ -249,7 +272,7 @@
|
||||
return { status: 200, body: {
|
||||
id: doc.id, status: doc.status, sealed: doc.status === STATUS.COMPLETED,
|
||||
filename: doc.title + (doc.status === STATUS.COMPLETED ? '_signed.pdf' : '.pdf'),
|
||||
pdfBase64: dd.data,
|
||||
pdfBase64: __blob.get(dd.data), // dd.data is the blob key; fetch the bytes
|
||||
} };
|
||||
}
|
||||
|
||||
@@ -292,7 +315,7 @@
|
||||
document: { id: doc.id, title: doc.title, status: doc.status },
|
||||
recipient: { id: r.id, email: r.email, name: r.name, role: r.role, signingStatus: r.signing_status },
|
||||
fields: myFields,
|
||||
pdfBase64: dd ? dd.data : null,
|
||||
pdfBase64: dd ? __blob.get(dd.data) : null, // dd.data is the blob key
|
||||
} };
|
||||
}
|
||||
|
||||
@@ -303,6 +326,7 @@
|
||||
if (doc.status !== STATUS.PENDING) conflict('document is not pending signature');
|
||||
if (r.signing_status === SIGNING.SIGNED) conflict('recipient already completed');
|
||||
if (r.signing_status === SIGNING.REJECTED) conflict('recipient already rejected');
|
||||
assertTurn(doc, r); // SEQUENTIAL: refuse until earlier signers have signed
|
||||
|
||||
var field = firstRow(query('SELECT * FROM fields WHERE id = ? AND document_id = ?', [req.params.fieldId, doc.id]));
|
||||
if (!field) notFound('field not found');
|
||||
@@ -363,6 +387,7 @@
|
||||
var r = rd.recipient, doc = rd.doc;
|
||||
if (doc.status !== STATUS.PENDING) conflict('document is not pending signature');
|
||||
if (r.signing_status === SIGNING.SIGNED) conflict('recipient already completed');
|
||||
assertTurn(doc, r); // SEQUENTIAL: refuse until earlier signers have signed
|
||||
|
||||
var pending = firstRow(query('SELECT COUNT(*) AS c FROM fields WHERE document_id = ? AND recipient_id = ? AND inserted = 0', [doc.id, r.id]));
|
||||
if (pending && Number(pending.c) > 0) bad('recipient has ' + pending.c + ' unsigned field(s)');
|
||||
@@ -407,11 +432,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
var stamped = __pdf.stamp(dd.data, JSON.stringify(stamps));
|
||||
var original = __blob.get(dd.data); // dd.data is the (initial) blob key
|
||||
var stamped = __pdf.stamp(original, JSON.stringify(stamps));
|
||||
var signed = __pdf.sign(stamped);
|
||||
|
||||
var t = now();
|
||||
exec('UPDATE document_data SET data = ? WHERE id = ?', [signed, dd.id]);
|
||||
// Persist the sealed PDF to a NEW blob key and point document_data.data at it;
|
||||
// initial_data still references the original key, so both survive on the seam.
|
||||
var sealedKey = 'doc/' + dd.id + '/sealed';
|
||||
__blob.put(sealedKey, signed);
|
||||
exec('UPDATE document_data SET data = ? WHERE id = ?', [sealedKey, dd.id]);
|
||||
exec('UPDATE documents SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?', [STATUS.COMPLETED, t, t, doc.id]);
|
||||
audit(doc.id, AUDIT.DOCUMENT_COMPLETED, { transactionId: id() }, actor);
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user