932923ee99
Nova Slides Render / render (push) Failing after 22s
---ci--- project: acdl phase: 6 milestone: v1.29 status: complete ---/ci---
408 lines
18 KiB
YAML
408 lines
18 KiB
YAML
# Nova Publish Pipeline — wheel + Lambda layer + Lambda zip + ECR kj
|
|
# image, all attached to a GitHub Release per tag (REQ-323, CAP-035,
|
|
# REQ-354, NFR-6, KJ-STATIC, D-239).
|
|
#
|
|
# This workflow is byte-identical across the production forge (GitHub
|
|
# Actions) and the dev forge (act_runner) — the same file is installed
|
|
# at .github/workflows/publish.yml and the mirror at
|
|
# <dev-forge>/workflows/publish.yml. Both copies must match exactly
|
|
# (asserted by tests/test_forge_action_byte_identical.py for the action
|
|
# and by the repo's byte-identical convention for workflows).
|
|
#
|
|
# NFR-6 (wheel/layer co-versioning): every tag publish affecting
|
|
# core/**, adapters/**, nova/**, or pyproject.toml publishes BOTH a
|
|
# wheel AND a Lambda layer with identical version strings. If either
|
|
# publish fails, the job fails and the release is blocked.
|
|
#
|
|
# REQ-323: CodeArtifact wheel + Lambda layer pipeline.
|
|
# REQ-354: per-tag GitHub Release attaching the Lambda token-vend zip,
|
|
# the Lambda layer zip, the Python wheel, and the ECR kj
|
|
# container image URI + digest, each with SHA-256 in the body.
|
|
# CAP-035: Lambda layer ARN version matches the nova-cli wheel version;
|
|
# the mapping is recorded in SSM /nova/layer/nova-cli/version.
|
|
# KJ-STATIC: the `kj` Go binary is built CGO_ENABLED=0 and asserted
|
|
# statically linked by `file(1)` before it is embedded in the
|
|
# ECR image. The build fails closed if `file kj` does not
|
|
# contain `statically linked` or does contain `shared library`.
|
|
# D-239: ECR tags reject `+`; the image tag uses `-` as the separator:
|
|
# `v1.29.x-kj-<kj-source-sha>`.
|
|
#
|
|
# Triggers:
|
|
# - push of a tag matching `v1.29.*` (the tag carries the version;
|
|
# REQ-354 criterion 1). Each tag produces an independent release
|
|
# (criterion 2 — previous tags' artifacts remain downloadable).
|
|
# - workflow_dispatch (manual republish, e.g. after a CodeArtifact
|
|
# provisioning fix)
|
|
#
|
|
# Wheel index selection (CodeArtifact default + fallback):
|
|
# - CodeArtifact mode: set the NOVA_CODEARTIFACT_DOMAIN repository
|
|
# secret (e.g. "nova"). The workflow runs
|
|
# `aws codeartifact login --tool twine --domain $NOVA_CODEARTIFACT_DOMAIN
|
|
# --repository nova-pypi` and twine uploads to the CodeArtifact pypi
|
|
# endpoint.
|
|
# - Fallback mode: leave NOVA_CODEARTIFACT_DOMAIN unset and provide
|
|
# TWINE_REPOSITORY_URL + TWINE_USERNAME + TWINE_PASSWORD repository
|
|
# secrets pointing at any PEP 503 simple index (a private package
|
|
# registry). twine uploads to TWINE_REPOSITORY_URL.
|
|
# See docs/codeartifact-provisioning.md for the required IAM grants
|
|
# + the fallback index shape.
|
|
#
|
|
# ECR image (kj substrate, REQ-354 criterion 3):
|
|
# - The `build-kj-image` job reads platform/abac/kj-version.txt
|
|
# (line 1 = version tag, line 2 = tree SHA, line 3 = source repo URL).
|
|
# - It fetches the kj Go source by tag (reliable; the pinned tree SHA
|
|
# is kept for traceability with v1.28 — see kj-version.txt comments).
|
|
# - It builds CGO_ENABLED=0, asserts KJ-STATIC via `file(1)`, packages
|
|
# the binary into public.ecr.aws/lambda/python:3.12-al2023 at
|
|
# /opt/kj/kj (chmod 0555, sbx_user:1051), and pushes to ECR with tag
|
|
# v1.29.x-kj-<kj-source-sha>. The tag is validated against
|
|
# ^[a-zA-Z0-9._-]+$ before push (D-239).
|
|
#
|
|
# Secrets / env:
|
|
# AWS_ROLE_ARN — OIDC role to assume (id-token: write)
|
|
# NOVA_CODEARTIFACT_DOMAIN — optional; when set, CodeArtifact mode
|
|
# TWINE_USERNAME — fallback-index upload user
|
|
# TWINE_PASSWORD — fallback-index upload password
|
|
# TWINE_REPOSITORY_URL — fallback-index upload URL
|
|
# AWS_DEFAULT_REGION (optional) — defaults to us-east-1
|
|
# NOVA_ECR_REPO — ECR repository URI for the kj image
|
|
# (e.g. 581513795199.dkr.ecr.us-east-1.
|
|
# amazonaws.com/nova-kj)
|
|
name: nova-publish
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v1.29.*"
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
id-token: write # OIDC federation to AWS
|
|
contents: write # create the GitHub Release + upload artifacts
|
|
|
|
jobs:
|
|
build-kj-image:
|
|
# KJ substrate — compile the kj Go binary static, package it into a
|
|
# public.ecr.aws/lambda/python:3.12-al2023 image at /opt/kj/kj, and
|
|
# push to ECR with tag v1.29.x-kj-<kj-source-sha> (D-239). Records
|
|
# image_uri + digest for the release body (REQ-354 criterion 4).
|
|
name: Build + push kj ECR image (KJ-STATIC, D-239)
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
image_uri: ${{ steps.ecr-push.outputs.image_uri }}
|
|
image_digest: ${{ steps.ecr-push.outputs.image_digest }}
|
|
image_tag: ${{ steps.ecr-push.outputs.image_tag }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-go@v5
|
|
with:
|
|
go-version: "1.22"
|
|
|
|
- name: Read kj version pin (platform/abac/kj-version.txt)
|
|
id: kj-ver
|
|
run: |
|
|
set -e
|
|
KJ_VERSION=$(sed -n '1p' platform/abac/kj-version.txt)
|
|
KJ_TREE_SHA=$(sed -n '2p' platform/abac/kj-version.txt)
|
|
KJ_REPO_URL=$(sed -n '3p' platform/abac/kj-version.txt)
|
|
echo "kj_version=${KJ_VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "kj_tree_sha=${KJ_TREE_SHA}" >> "$GITHUB_OUTPUT"
|
|
echo "kj_repo_url=${KJ_REPO_URL}" >> "$GITHUB_OUTPUT"
|
|
echo "Pinned kj: version=${KJ_VERSION} tree_sha=${KJ_TREE_SHA} repo=${KJ_REPO_URL}"
|
|
|
|
- name: Fetch kj Go source at tag v0.0.3
|
|
env:
|
|
KJ_REPO_URL: ${{ steps.kj-ver.outputs.kj_repo_url }}
|
|
KJ_VERSION: ${{ steps.kj-ver.outputs.kj_version }}
|
|
run: |
|
|
set -e
|
|
# The pinned tree SHA (line 2) 404s as a commit; the build
|
|
# fetches by tag, which dereferences to a real commit
|
|
# (verified: 924a6af2474523c4e27e3a826248c91c8fe1d1cf).
|
|
rm -rf kj-src
|
|
git clone --depth 1 --branch "${KJ_VERSION}" \
|
|
"${KJ_REPO_URL}" kj-src
|
|
|
|
- name: Build kj (CGO_ENABLED=0 — KJ-STATIC)
|
|
working-directory: kj-src
|
|
run: |
|
|
set -e
|
|
# Resolve the tagged commit SHA — this is the source SHA
|
|
# embedded in the ECR image tag (REQ-354 criterion 3).
|
|
KJ_SOURCE_SHA=$(git rev-parse HEAD)
|
|
echo "kj_source_sha=${KJ_SOURCE_SHA}" >> "$GITHUB_ENV"
|
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
|
go build -ldflags="-s -w" -o kj ./...
|
|
file kj
|
|
|
|
- name: Assert kj is statically linked (KJ-STATIC CI gate)
|
|
working-directory: kj-src
|
|
run: |
|
|
set -e
|
|
# KJ-STATIC: file(1) MUST report `statically linked` and MUST
|
|
# NOT report `shared library`. Fail closed otherwise — this
|
|
# is the mechanical enforcement of KJ-STATIC (not human review).
|
|
FILE_OUT=$(file kj)
|
|
echo "$FILE_OUT"
|
|
case "$FILE_OUT" in
|
|
*statically\ linked*) ;;
|
|
*) echo "FAIL (KJ-STATIC): kj is not statically linked"; exit 1 ;;
|
|
esac
|
|
case "$FILE_OUT" in
|
|
*shared\ library*)
|
|
echo "FAIL (KJ-STATIC): kj links a shared library"; exit 1 ;;
|
|
*) ;;
|
|
esac
|
|
# readelf defense-in-depth: assert no NEEDED entries.
|
|
if readelf -d kj 2>/dev/null | grep -q NEEDED; then
|
|
echo "FAIL (KJ-STATIC): readelf -d reports NEEDED entries"; exit 1
|
|
fi
|
|
echo "KJ-STATIC assertion passed."
|
|
|
|
- name: Configure AWS credentials (OIDC)
|
|
uses: aws-actions/configure-aws-credentials@v4
|
|
with:
|
|
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
|
aws-region: ${{ secrets.AWS_DEFAULT_REGION || 'us-east-1' }}
|
|
|
|
- name: Log in to ECR
|
|
env:
|
|
NOVA_ECR_REPO: ${{ secrets.NOVA_ECR_REPO }}
|
|
run: |
|
|
set -e
|
|
# NOVA_ECR_REPO is the full repo URI, e.g.
|
|
# 581513795199.dkr.ecr.us-east-1.amazonaws.com/nova-kj
|
|
REGISTRY=$(echo "$NOVA_ECR_REPO" | cut -d/ -f1)
|
|
aws ecr get-login-password --region "${AWS_REGION}" \
|
|
| docker login --username AWS --password-stdin "$REGISTRY"
|
|
|
|
- name: Build + push kj image to ECR (D-239)
|
|
id: ecr-push
|
|
env:
|
|
NOVA_ECR_REPO: ${{ secrets.NOVA_ECR_REPO }}
|
|
KJ_SOURCE_SHA: ${{ env.kj_source_sha }}
|
|
working-directory: kj-src
|
|
run: |
|
|
set -e
|
|
# D-239: ECR tags reject `+`; use `-` separator. The tag is
|
|
# v1.29.x-kj-<kj-source-sha> and is validated against
|
|
# ^[a-zA-Z0-9._-]+$ before push.
|
|
IMAGE_TAG="v1.29.x-kj-${KJ_SOURCE_SHA}"
|
|
if ! echo "$IMAGE_TAG" | grep -Eq '^[a-zA-Z0-9._-]+$'; then
|
|
echo "FAIL (D-239): invalid ECR tag: ${IMAGE_TAG}"
|
|
exit 1
|
|
fi
|
|
IMAGE_URI="${NOVA_ECR_REPO}:${IMAGE_TAG}"
|
|
echo "Pushing image: ${IMAGE_URI}"
|
|
# Stage the binary into a build context root.
|
|
rm -rf imgctx && mkdir -p imgctx/opt/kj
|
|
cp kj imgctx/opt/kj/kj
|
|
chmod 0555 imgctx/opt/kj/kj
|
|
printf '%s\n' \
|
|
'FROM public.ecr.aws/lambda/python:3.12-al2023' \
|
|
'COPY --chown=sbx_user:1051 --chmod=0555 opt/kj/kj /opt/kj/kj' \
|
|
> imgctx/Dockerfile
|
|
docker build -t "$IMAGE_URI" imgctx
|
|
docker push "$IMAGE_URI" >/tmp/docker-push.log 2>&1
|
|
cat /tmp/docker-push.log
|
|
# Extract the registry digest via `docker inspect` (the
|
|
# canonical source — push output wording varies by client).
|
|
IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
|
|
"$IMAGE_URI" | sed 's/.*@//')
|
|
echo "image_uri=${IMAGE_URI}" >> "$GITHUB_OUTPUT"
|
|
echo "image_digest=${IMAGE_DIGEST}" >> "$GITHUB_OUTPUT"
|
|
echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "Pushed ${IMAGE_URI} @ ${IMAGE_DIGEST}"
|
|
|
|
publish:
|
|
name: Publish wheel + Lambda layer + Lambda zip + Release
|
|
runs-on: ubuntu-latest
|
|
needs: build-kj-image
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Configure AWS credentials (OIDC)
|
|
uses: aws-actions/configure-aws-credentials@v4
|
|
with:
|
|
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
|
aws-region: ${{ secrets.AWS_DEFAULT_REGION || 'us-east-1' }}
|
|
|
|
- name: Install build + publish tools
|
|
run: pip install build twine
|
|
|
|
- name: Compute version from pyproject.toml
|
|
id: ver
|
|
run: |
|
|
set -e
|
|
VERSION=$(python -c 'import tomllib;print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "Nova version: $VERSION"
|
|
|
|
- name: Build wheel
|
|
run: |
|
|
set -e
|
|
python -m build --wheel
|
|
ls -1 dist/
|
|
|
|
- name: Upload wheel to index (CodeArtifact default + fallback)
|
|
id: wheel
|
|
env:
|
|
NOVA_CODEARTIFACT_DOMAIN: ${{ secrets.NOVA_CODEARTIFACT_DOMAIN }}
|
|
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
|
|
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
|
|
TWINE_REPOSITORY_URL: ${{ secrets.TWINE_REPOSITORY_URL }}
|
|
run: |
|
|
set -e
|
|
# CodeArtifact mode: log in to the domain's pypi repository.
|
|
if [ -n "$NOVA_CODEARTIFACT_DOMAIN" ]; then
|
|
echo "CodeArtifact mode: domain=$NOVA_CODEARTIFACT_DOMAIN repository=nova-pypi"
|
|
aws codeartifact login --tool twine \
|
|
--domain "$NOVA_CODEARTIFACT_DOMAIN" --repository nova-pypi
|
|
else
|
|
echo "Fallback-index mode: uploading to TWINE_REPOSITORY_URL"
|
|
if [ -z "$TWINE_REPOSITORY_URL" ] || [ -z "$TWINE_USERNAME" ] || [ -z "$TWINE_PASSWORD" ]; then
|
|
echo "FAIL: NOVA_CODEARTIFACT_DOMAIN is unset and one of TWINE_REPOSITORY_URL/TWINE_USERNAME/TWINE_PASSWORD is missing."
|
|
exit 1
|
|
fi
|
|
fi
|
|
# Idempotent upload: a re-run for the same version may hit
|
|
# "file already exists" on the index. Treat that as success.
|
|
# Capture both attempts' output so a genuine failure (auth,
|
|
# network, invalid package) is NOT masked as success — NFR-6
|
|
# requires the job to fail if the wheel publish fails.
|
|
if twine upload "dist/nova-${{ steps.ver.outputs.version }}-*.whl" 2>&1 | tee /tmp/twine.log; then
|
|
echo "uploaded=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
# Retry once; the first attempt may have failed with a
|
|
# transient error OR with "already exists" (a re-run).
|
|
twine upload "dist/nova-${{ steps.ver.outputs.version }}-*.whl" 2>&1 | tee -a /tmp/twine.log || true
|
|
if grep -qi "already exist" /tmp/twine.log 2>/dev/null; then
|
|
echo "Wheel already present on the index — treating as success (idempotent)."
|
|
echo "uploaded=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "FAIL: wheel upload failed (not an idempotent re-run)." >&2
|
|
cat /tmp/twine.log >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
- name: Build Lambda layer
|
|
run: |
|
|
set -e
|
|
rm -rf layer
|
|
mkdir -p layer/python
|
|
# Install the wheel we just built + the identity extras' deps
|
|
# so the layer carries argon2-cffi, cryptography, pyjwt.
|
|
pip install --target layer/python/ \
|
|
"dist/nova-${{ steps.ver.outputs.version }}-*.whl" \
|
|
argon2-cffi cryptography pyjwt
|
|
( cd layer && zip -r ../nova-cli-layer-v1.29.x.zip python/ )
|
|
ls -lh nova-cli-layer-v1.29.x.zip
|
|
|
|
- name: Publish Lambda layer
|
|
id: layer
|
|
run: |
|
|
set -e
|
|
ARN=$(aws lambda publish-layer-version \
|
|
--layer-name nova-cli \
|
|
--zip-file fileb://nova-cli-layer-v1.29.x.zip \
|
|
--compatible-runtimes python3.12 \
|
|
--compatible-architectures x86_64 \
|
|
--description "nova-cli v${{ steps.ver.outputs.version }}" \
|
|
--query LayerVersionArn --output text)
|
|
echo "arn=$ARN" >> "$GITHUB_OUTPUT"
|
|
echo "Published Lambda layer: $ARN"
|
|
|
|
- name: Record SSM version↔ARN mapping (CAP-035)
|
|
run: |
|
|
set -e
|
|
aws ssm put-parameter \
|
|
--name /nova/layer/nova-cli/version \
|
|
--value "${{ steps.ver.outputs.version }}:${{ steps.layer.outputs.arn }}" \
|
|
--type String --overwrite
|
|
echo "SSM /nova/layer/nova-cli/version = ${{ steps.ver.outputs.version }}:${{ steps.layer.outputs.arn }}"
|
|
|
|
- name: Build Lambda token-vend zip (nova-lambda-token-vend-v1.29.x.zip)
|
|
run: |
|
|
set -e
|
|
# Package the nova-idp-token-vend Lambda handler (the dual-use
|
|
# module core/lambda/nova_idp_token_vend.py) plus the core/
|
|
# package modules it imports at runtime (core.policy_engine,
|
|
# core.abac_evaluator, core.kms_signing). The zip root mirrors
|
|
# the repo layout so `import core.lambda.nova_idp_token_vend`
|
|
# resolves inside the Lambda execution environment.
|
|
rm -rf lambdazip
|
|
mkdir -p lambdazip/core/lambda
|
|
cp core/lambda/__init__.py lambdazip/core/lambda/__init__.py
|
|
cp core/lambda/nova_idp_token_vend.py \
|
|
lambdazip/core/lambda/nova_idp_token_vend.py
|
|
# Carry the core/ modules the handler imports lazily.
|
|
cp core/__init__.py lambdazip/core/__init__.py 2>/dev/null || true
|
|
cp core/policy_engine.py lambdazip/core/policy_engine.py 2>/dev/null || true
|
|
cp core/abac_evaluator.py lambdazip/core/abac_evaluator.py 2>/dev/null || true
|
|
cp core/kms_signing.py lambdazip/core/kms_signing.py 2>/dev/null || true
|
|
( cd lambdazip && zip -r ../nova-lambda-token-vend-v1.29.x.zip . )
|
|
ls -lh nova-lambda-token-vend-v1.29.x.zip
|
|
|
|
- name: Compute SHA-256 of all release artifacts
|
|
id: sha
|
|
run: |
|
|
set -e
|
|
sha256sum nova-lambda-token-vend-v1.29.x.zip \
|
|
> /tmp/sha-lambda.txt
|
|
sha256sum nova-cli-layer-v1.29.x.zip \
|
|
> /tmp/sha-layer.txt
|
|
sha256sum dist/nova-${{ steps.ver.outputs.version }}-*.whl \
|
|
> /tmp/sha-wheel.txt
|
|
{
|
|
echo "## Artifact SHA-256 (REQ-354)"
|
|
echo ""
|
|
echo "### nova-lambda-token-vend-v1.29.x.zip"
|
|
echo '```'
|
|
cat /tmp/sha-lambda.txt
|
|
echo '```'
|
|
echo ""
|
|
echo "### nova-cli-layer-v1.29.x.zip"
|
|
echo '```'
|
|
cat /tmp/sha-layer.txt
|
|
echo '```'
|
|
echo ""
|
|
echo "### nova-${{ steps.ver.outputs.version }}-py3-none-any.whl"
|
|
echo '```'
|
|
cat /tmp/sha-wheel.txt
|
|
echo '```'
|
|
echo ""
|
|
echo "### ECR kj image (REQ-354 criterion 3/4)"
|
|
echo "- URI: \`${{ needs.build-kj-image.outputs.image_uri }}\`"
|
|
echo "- digest: \`${{ needs.build-kj-image.outputs.image_digest }}\`"
|
|
echo "- tag: \`${{ needs.build-kj-image.outputs.image_tag }}\`"
|
|
echo ""
|
|
} > /tmp/release-body.md
|
|
echo "body_path=/tmp/release-body.md" >> "$GITHUB_OUTPUT"
|
|
echo "--- Release body ---"
|
|
cat /tmp/release-body.md
|
|
|
|
- name: Create GitHub Release + attach artifacts (REQ-354)
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
# Use the pushed tag as the release tag.
|
|
tag_name: ${{ github.ref_name }}
|
|
name: Nova ${{ github.ref_name }}
|
|
body_path: ${{ steps.sha.outputs.body_path }}
|
|
files: |
|
|
nova-lambda-token-vend-v1.29.x.zip
|
|
nova-cli-layer-v1.29.x.zip
|
|
dist/nova-${{ steps.ver.outputs.version }}-*.whl
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Fail job if either publish failed (REQ-323 AC)
|
|
if: ${{ steps.wheel.outputs.uploaded != 'true' || steps.layer.outputs.arn == '' }}
|
|
run: |
|
|
echo "FAIL: wheel uploaded=${{ steps.wheel.outputs.uploaded }} layer_arn=${{ steps.layer.outputs.arn }}"
|
|
exit 1 |