Files
hyperframes/.github/workflows/publish.yml
Miguel Ángel b501dc4e1b feat(cli): lambda deploy works from the npm install, no repo checkout (#5866)
* feat(cli): lambda deploy works from the npm install, no repo checkout

Each release builds the Lambda handler zip on Linux, attaches it to the
GitHub release, and pins its URL and sha256 (plus the SAM template) in
@hyperframes/aws-lambda before npm publish. Outside a checkout, lambda
deploy downloads that zip, refuses it on a sha256 mismatch, and deploys
it with the packaged template. With neither a checkout nor a pinned zip
it fails up front with one message naming every way out.

* fix(cli): lambda deploy test mocks the pinned-handler reader, comments fit the checks

* test(cli): sam tests deploy from a SAM directory, not the repo root

* chore(aws-lambda): raise the package size budget for the pinned handler template

Measured with pnpm@10.17.1 pack on Linux. Release state (prebuilt/ written by pin-handler-zip): 77,307 packed / 285,654 unpacked. CI state (no prebuilt/): 70,104 / 253,390. Packed is the check's 5% headroom over the release size. Unpacked is 292,000 (2.2% over release) because a 5% budget would put the CI pack under the 85% ratchet.

* test(cli): anchor the docs link check in the lambda deploy fallback test

* fix(cli): lambda deploy logs the verified handler sha256 and keeps a presigned query out of errors

* fix(cli): a redirect-limit download error keeps a signed query out of its message

* test(cli): the redirect-limit leak test types its rejection

* docs(aws-lambda): header comments cut to what the code cannot say

* chore(aws-lambda): size budget fits both the CI and the release package
2026-10-19 21:41:50 +00:00

253 lines
10 KiB
YAML

name: Publish to npm
permissions: {}
on:
push:
tags:
# Stable tags are created only by a merged, reviewed release PR. Direct
# tag pushes are reserved for prerelease channels.
- "v*-*"
pull_request:
types: [closed]
branches: [main]
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
timeout-minutes: 20
environment: npm-publish
permissions:
contents: write
id-token: write
env:
EXPECTED_RELEASE_SHA: >-
${{ github.event_name == 'pull_request'
&& github.event.pull_request.merge_commit_sha
|| github.sha }}
# Stable releases come only from reviewed release PRs. Tag pushes are
# prerelease-only because the trigger pattern requires a hyphenated version.
if: >-
github.event_name == 'push' ||
(github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.head.ref, 'release/v'))
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
# Pin every release to an immutable ref. In particular, a merged
# release PR event exposes main as github.ref, which may advance
# before this job starts; use the event's exact merge commit instead.
ref: ${{ env.EXPECTED_RELEASE_SHA }}
- name: Verify immutable release checkout
run: |
ACTUAL_SHA="$(git rev-parse HEAD)"
EXPECTED_COMMIT_SHA="$(git rev-parse "${EXPECTED_RELEASE_SHA}^{commit}")"
if [ "$ACTUAL_SHA" != "$EXPECTED_COMMIT_SHA" ]; then
echo "::error::Expected release commit $EXPECTED_COMMIT_SHA, checked out $ACTUAL_SHA"
exit 1
fi
- name: Resolve version
id: version
env:
EVENT_NAME: ${{ github.event_name }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
if [ "$EVENT_NAME" = "push" ]; then
VERSION="${GITHUB_REF_NAME#v}"
else
BRANCH="${PR_HEAD_REF}"
VERSION="${BRANCH#release/v}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
# Detect pre-release tag (e.g. 0.1.16-alpha.1 → alpha, 0.1.16-beta.2 → beta)
if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then
DIST_TAG="${BASH_REMATCH[1]}"
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
DIST_TAG="latest"
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
echo "dist_tag=${DIST_TAG}" >> "$GITHUB_OUTPUT"
echo "Resolved version=${VERSION} dist_tag=${DIST_TAG}"
- name: Validate release channel
env:
VERSION: ${{ steps.version.outputs.version }}
DIST_TAG: ${{ steps.version.outputs.dist_tag }}
EVENT_NAME: ${{ github.event_name }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: node scripts/validate-release-channel.mjs
- name: Create release tag
if: github.event_name == 'pull_request'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
TAG="v$VERSION"
EXPECTED_TAG_SHA="$(git rev-parse HEAD)"
verify_remote_tag() {
git fetch --force --no-tags origin "+refs/tags/$TAG:refs/tags/$TAG"
ACTUAL_TAG_SHA="$(git rev-parse "refs/tags/$TAG^{commit}")"
if [ "$ACTUAL_TAG_SHA" != "$EXPECTED_TAG_SHA" ]; then
echo "::error::Release tag $TAG points to $ACTUAL_TAG_SHA, expected $EXPECTED_TAG_SHA"
exit 1
fi
echo "Release tag $TAG already exists at the expected commit — skipping"
}
if [ -n "$(git ls-remote --refs origin "refs/tags/$TAG")" ]; then
verify_remote_tag
else
git tag --no-sign "$TAG" "$EXPECTED_TAG_SHA"
if ! git push origin "refs/tags/$TAG"; then
# A concurrent retry may have created the tag after ls-remote.
git tag -d "$TAG"
verify_remote_tag
fi
fi
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
registry-url: "https://registry.npmjs.org"
- run: corepack enable
- run: corepack prepare pnpm@10.17.1 --activate
- run: bun install --frozen-lockfile
- run: bun run build
# Before `npm publish`, so @hyperframes/aws-lambda pins this exact zip.
- name: Build Lambda handler zip
id: lambda_zip
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
REPO: ${{ github.repository }}
run: |
# A retried run reuses the zip already on the release, so the pin still matches it.
if gh release download "v${VERSION}" --repo "$REPO" --pattern hyperframes-lambda-handler.zip --dir dist; then
echo "attached=true" >> "$GITHUB_OUTPUT"
else
bun run --cwd packages/aws-lambda build:zip
fi
bun packages/aws-lambda/scripts/pin-handler-zip.ts
- run: bun run verify:packed-manifests
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
PRERELEASE: ${{ steps.version.outputs.prerelease }}
REPO: ${{ github.repository }}
run: |
NOTES_FILE="releases/v${VERSION}.md"
# Skip if release already exists (idempotent re-runs)
if gh release view "v${VERSION}" --repo "$REPO" >/dev/null 2>&1; then
echo "Release v${VERSION} already exists — skipping"
else
FLAGS=(--repo "$REPO" --title "v${VERSION}")
if [ -f "$NOTES_FILE" ]; then
FLAGS+=(--notes-file "$NOTES_FILE")
else
echo "No reviewed release notes found at $NOTES_FILE — using GitHub generated notes"
FLAGS+=(--generate-notes)
fi
if [ "$PRERELEASE" = "true" ]; then
FLAGS+=(--prerelease)
fi
gh release create "v${VERSION}" "${FLAGS[@]}"
fi
- name: Attach Lambda handler zip to release
if: steps.lambda_zip.outputs.attached != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
REPO: ${{ github.repository }}
run: gh release upload "v${VERSION}" dist/hyperframes-lambda-handler.zip dist/hyperframes-lambda-handler.sha256 --repo "$REPO"
- name: Publish packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
DIST_TAG: ${{ steps.version.outputs.dist_tag }}
run: |
FAILED=0
publish_pkg() {
local filter="$1"
local name="$2"
# Check if this version is already published
if npm view "${name}@${VERSION}" version >/dev/null 2>&1; then
echo "⏭️ ${name}@${VERSION} already published — skipping"
return 0
fi
echo "📦 Publishing ${name}@${VERSION}..."
if pnpm --filter "$filter" publish --access public --no-git-checks --tag "$DIST_TAG"; then
echo "✅ ${name}@${VERSION} published"
else
echo "❌ ${name}@${VERSION} failed to publish"
FAILED=1
fi
}
publish_pkg "@hyperframes/parsers" "@hyperframes/parsers"
publish_pkg "@hyperframes/lint" "@hyperframes/lint"
publish_pkg "@hyperframes/studio-server" "@hyperframes/studio-server"
publish_pkg "@hyperframes/core" "@hyperframes/core"
publish_pkg "@hyperframes/inspector" "@hyperframes/inspector"
publish_pkg "@hyperframes/sdk" "@hyperframes/sdk"
publish_pkg "@hyperframes/engine" "@hyperframes/engine"
publish_pkg "@hyperframes/player" "@hyperframes/player"
publish_pkg "@hyperframes/producer" "@hyperframes/producer"
publish_pkg "@hyperframes/shader-transitions" "@hyperframes/shader-transitions"
publish_pkg "@hyperframes/studio" "@hyperframes/studio"
publish_pkg "@hyperframes/aws-lambda" "@hyperframes/aws-lambda"
publish_pkg "@hyperframes/gcp-cloud-run" "@hyperframes/gcp-cloud-run"
# CLI is @hyperframes/cli in the monorepo but published as unscoped "hyperframes" on npm.
# Rewrite the name in package.json before publishing, then use npm publish directly
# since pnpm --filter won't match the rewritten name.
if npm view "hyperframes@${VERSION}" version >/dev/null 2>&1; then
echo "⏭️ hyperframes@${VERSION} already published — skipping"
else
node -e "
const fs = require('fs');
const p = 'packages/cli/package.json';
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.name = 'hyperframes';
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
"
echo "📦 Publishing hyperframes@${VERSION}..."
if (cd packages/cli && npm publish --access public --tag "$DIST_TAG"); then
echo "✅ hyperframes@${VERSION} published"
else
echo "❌ hyperframes@${VERSION} failed to publish"
FAILED=1
fi
fi
if [ "$FAILED" -ne 0 ]; then
echo "::error::One or more packages failed to publish"
exit 1
fi
- name: Build portable agent plugin
run: node scripts/package-agent-plugin.mjs
- name: Attach portable plugin to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
REPO: ${{ github.repository }}
run: gh release upload "v${VERSION}" dist/hyperframes-agent-plugin.zip dist/hyperframes-agent-plugin.sha256 dist/hyperframes-agent-plugin.json --repo "$REPO" --clobber