03adaa80a6
Byte-identical .github/workflows/publish.yml + mirror on the dev forge
(<dev-forge>/workflows/publish.yml) — same file content, installed in
both locations per the repo's byte-identical workflow convention.
NFR-6 (wheel/layer co-versioning): on push to main affecting core/**,
adapters/**, nova/**, or pyproject.toml, the workflow publishes BOTH a
wheel AND a Lambda layer with identical version strings. If either
publish fails, the job fails and the merge is blocked (REQ-323 AC).
Steps:
- actions/checkout@v4 + actions/setup-python@v5 (python 3.12)
- aws-actions/configure-aws-credentials@v4 (OIDC, role-to-assume from
AWS_ROLE_ARN secret, id-token: write)
- pip install build twine
- compute version: tomllib.load(pyproject.toml)["project"]["version"]
→ steps.ver.outputs.version (e.g. 1.14.0)
- python -m build --wheel
- twine upload dist/nova-<ver>-*.whl with two modes:
* CodeArtifact: NOVA_CODEARTIFACT_DOMAIN set →
aws codeartifact login --tool twine --domain $DOMAIN --repository
nova-pypi
* Fallback: NOVA_CODEARTIFACT_DOMAIN unset → TWINE_REPOSITORY_URL +
TWINE_USERNAME + TWINE_PASSWORD secrets (any PEP 503 index)
Idempotent: a re-upload that hits "file already exists" is treated as
success.
- build Lambda layer: pip install --target layer/python/ the wheel +
argon2-cffi + cryptography + pyjwt, then zip -r nova-layer.zip python/
- aws lambda publish-layer-version --layer-name nova-cli
--compatible-runtimes python3.12 --compatible-architectures x86_64
--description "nova-cli v<ver>" → steps.layer.outputs.arn
- aws ssm put-parameter /nova/layer/nova-cli/version =
"<wheel-version>:<layer-arn>" (CAP-035)
- final guard step fails the job if wheel uploaded!=true or layer arn
is empty
permissions: id-token: write (OIDC), contents: write (tag).
Secrets documented in the workflow header comments.
---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: backend-engineer
---/ci---
165 lines
6.8 KiB
YAML
165 lines
6.8 KiB
YAML
# Nova Publish Pipeline — wheel + Lambda layer (REQ-323, CAP-035, NFR-6)
|
|
#
|
|
# 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 merge to main 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 merge is blocked.
|
|
#
|
|
# REQ-323: CodeArtifact wheel + Lambda layer pipeline.
|
|
# CAP-035: Lambda layer ARN version matches the nova-cli wheel version;
|
|
# the mapping is recorded in SSM /nova/layer/nova-cli/version.
|
|
#
|
|
# Triggers:
|
|
# - push to main when core/**, adapters/**, nova/**, or pyproject.toml
|
|
# changed (the surfaces that ship in the wheel + layer)
|
|
# - 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.
|
|
#
|
|
# 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
|
|
name: nova-publish
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- "core/**"
|
|
- "adapters/**"
|
|
- "nova/**"
|
|
- "pyproject.toml"
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
id-token: write # OIDC federation to AWS
|
|
contents: write # tag the release
|
|
|
|
jobs:
|
|
publish:
|
|
name: Publish wheel + Lambda layer
|
|
runs-on: ubuntu-latest
|
|
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.
|
|
twine upload "dist/nova-${{ steps.ver.outputs.version }}-*.whl" \
|
|
|| twine upload "dist/nova-${{ steps.ver.outputs.version }}-*.whl" 2>&1 | tee /tmp/twine.log
|
|
if grep -qi "already exist" /tmp/twine.log 2>/dev/null; then
|
|
echo "Wheel already present on the index — treating as success (idempotent)."
|
|
fi
|
|
echo "uploaded=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- 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-layer.zip python/ )
|
|
ls -lh nova-layer.zip
|
|
|
|
- name: Publish Lambda layer
|
|
id: layer
|
|
run: |
|
|
set -e
|
|
ARN=$(aws lambda publish-layer-version \
|
|
--layer-name nova-cli \
|
|
--zip-file fileb://nova-layer.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: 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 |