From 03adaa80a69b83c6027c60ebe9c45b47c2239bd9 Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Wed, 19 Aug 2026 22:34:28 +0000 Subject: [PATCH] =?UTF-8?q?feat(P01):=20publish=20workflow=20=E2=80=94=20w?= =?UTF-8?q?heel=20+=20Lambda=20layer=20(REQ-323,=20CAP-035,=20backend-engi?= =?UTF-8?q?neer)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Byte-identical .github/workflows/publish.yml + mirror on the 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--*.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" → steps.layer.outputs.arn - aws ssm put-parameter /nova/layer/nova-cli/version = ":" (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--- --- .gitea/workflows/publish.yml | 165 ++++++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 165 ++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 .gitea/workflows/publish.yml create mode 100644 .github/workflows/publish.yml diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..e9ff59e --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,165 @@ +# 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 +# /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 \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e9ff59e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,165 @@ +# 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 +# /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 \ No newline at end of file