fix: add to gitignore and remove tracking of files moved at build time #14
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release @ai-primitives-hub packages to npmjs | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| release_type: | |
| description: "Semantic version bump" | |
| required: true | |
| default: "prerelease" | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| - prerelease | |
| preid: | |
| description: "Prerelease id (only used when release-type is prerelease)" | |
| required: false | |
| default: "alpha" | |
| type: string | |
| packages: | |
| description: "Packages to release" | |
| required: true | |
| default: "all" | |
| type: choice | |
| options: | |
| - all | |
| - core | |
| - infra | |
| - app | |
| - cli | |
| npm_tag: | |
| description: "npm distribution tag" | |
| required: true | |
| default: "next" | |
| type: choice | |
| options: | |
| - latest | |
| - next | |
| - alpha | |
| - beta | |
| dry_run: | |
| description: "Preview only: validate and build without pushing a branch or creating a PR" | |
| required: false | |
| default: false | |
| type: boolean | |
| skip_checks: | |
| description: "Skip lint and test gates (the publication build still runs)" | |
| required: false | |
| default: false | |
| type: boolean | |
| pull_request: | |
| branches: | |
| - main | |
| types: | |
| - closed | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| env: | |
| NODE_VERSION: "24" | |
| jobs: | |
| prepare: | |
| name: Prepare release pull request | |
| if: ${{ github.event_name == 'workflow_dispatch' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: pnpm | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: Validate release request | |
| env: | |
| RELEASE_TYPE: ${{ inputs.release_type }} | |
| PREID: ${{ inputs.preid }} | |
| PACKAGES: ${{ inputs.packages }} | |
| NPM_TAG: ${{ inputs.npm_tag }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then | |
| echo "Package releases must be prepared from main (received $GITHUB_REF)." >&2 | |
| exit 1 | |
| fi | |
| case "$RELEASE_TYPE" in | |
| patch|minor|major|prerelease) ;; | |
| *) echo "Unsupported release type: $RELEASE_TYPE" >&2; exit 1 ;; | |
| esac | |
| case "$PACKAGES" in | |
| all|core|infra|app|cli) ;; | |
| *) echo "Unsupported package selection: $PACKAGES" >&2; exit 1 ;; | |
| esac | |
| case "$NPM_TAG" in | |
| latest|next|alpha|beta) ;; | |
| *) echo "Unsupported npm tag: $NPM_TAG" >&2; exit 1 ;; | |
| esac | |
| if [[ "$RELEASE_TYPE" == "prerelease" && ! "$PREID" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then | |
| echo "Prerelease id must contain only letters, numbers, '.', '_' or '-'." >&2 | |
| exit 1 | |
| fi | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile --prefer-offline | |
| - name: Determine package selector | |
| id: selector | |
| env: | |
| PACKAGES: ${{ inputs.packages }} | |
| run: | | |
| set -euo pipefail | |
| case "$PACKAGES" in | |
| all) | |
| selector='@ai-primitives-hub/*' | |
| build_selector='@ai-primitives-hub/*...' | |
| names='core infra app cli' | |
| ;; | |
| core|infra|app|cli) | |
| selector="@ai-primitives-hub/$PACKAGES" | |
| build_selector="@ai-primitives-hub/$PACKAGES..." | |
| names="$PACKAGES" | |
| ;; | |
| *) | |
| echo "Unsupported package selection: $PACKAGES" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| { | |
| echo "selector=$selector" | |
| echo "build_selector=$build_selector" | |
| echo "names=$names" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create release branch | |
| id: branch | |
| env: | |
| PACKAGES: ${{ inputs.packages }} | |
| NPM_TAG: ${{ inputs.npm_tag }} | |
| run: | | |
| set -euo pipefail | |
| release_branch="release/packages/$PACKAGES/$NPM_TAG/$GITHUB_RUN_ID" | |
| git switch --create "$release_branch" | |
| echo "name=$release_branch" >> "$GITHUB_OUTPUT" | |
| - name: Bump package versions | |
| env: | |
| RELEASE_TYPE: ${{ inputs.release_type }} | |
| PREID: ${{ inputs.preid }} | |
| SELECTOR: ${{ steps.selector.outputs.selector }} | |
| run: | | |
| set -euo pipefail | |
| version_args=("$RELEASE_TYPE") | |
| if [[ "$RELEASE_TYPE" == "prerelease" ]]; then | |
| version_args+=(--preid "$PREID") | |
| fi | |
| pnpm version "${version_args[@]}" -r --filter "$SELECTOR" --no-git-tag-version | |
| - name: Update lockfile | |
| run: pnpm install --lockfile-only --ignore-scripts --prefer-offline | |
| - name: Commit version bump | |
| run: | | |
| set -euo pipefail | |
| git add pnpm-lock.yaml packages/*/package.json | |
| if git diff --cached --quiet; then | |
| echo "The requested release did not change any package versions." >&2 | |
| exit 1 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git commit -m "chore(release): prepare @ai-primitives-hub packages" | |
| - name: Build release packages | |
| env: | |
| BUILD_SELECTOR: ${{ steps.selector.outputs.build_selector }} | |
| run: pnpm --filter "$BUILD_SELECTOR" build | |
| - name: Lint and test release packages | |
| if: ${{ !inputs.skip_checks }} | |
| env: | |
| BUILD_SELECTOR: ${{ steps.selector.outputs.build_selector }} | |
| run: | | |
| set -euo pipefail | |
| pnpm --filter "$BUILD_SELECTOR" lint | |
| pnpm --filter "$BUILD_SELECTOR" test | |
| - name: Stage release build artifact | |
| if: ${{ !inputs.dry_run }} | |
| env: | |
| PACKAGE_NAMES: ${{ steps.selector.outputs.names }} | |
| NPM_TAG: ${{ inputs.npm_tag }} | |
| run: | | |
| set -euo pipefail | |
| rm -rf .release-artifacts | |
| PACKAGE_NAMES="$PACKAGE_NAMES" NPM_TAG="$NPM_TAG" node <<'NODE' | |
| const fs = require('node:fs'); | |
| const path = require('node:path'); | |
| const childProcess = require('node:child_process'); | |
| const names = process.env.PACKAGE_NAMES.split(/\s+/).filter(Boolean); | |
| const packages = names.map((directory) => { | |
| const packageJsonPath = path.join('packages', directory, 'package.json'); | |
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); | |
| const distPath = path.join('packages', directory, 'dist'); | |
| if (!fs.existsSync(distPath)) { | |
| throw new Error(`Expected build output is missing: ${distPath}`); | |
| } | |
| const stagedDistPath = path.join('.release-artifacts', 'packages', directory, 'dist'); | |
| fs.mkdirSync(path.dirname(stagedDistPath), { recursive: true }); | |
| fs.cpSync(distPath, stagedDistPath, { recursive: true }); | |
| return { | |
| directory, | |
| name: packageJson.name, | |
| version: packageJson.version, | |
| }; | |
| }); | |
| const manifest = { | |
| formatVersion: 1, | |
| runId: Number(process.env.GITHUB_RUN_ID), | |
| sourceCommit: childProcess.execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(), | |
| npmTag: process.env.NPM_TAG, | |
| packages, | |
| }; | |
| fs.writeFileSync('.release-artifacts/release-manifest.json', `${JSON.stringify(manifest, null, 2)}\n`); | |
| NODE | |
| - name: Upload release build artifact | |
| if: ${{ !inputs.dry_run }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-packages | |
| path: .release-artifacts | |
| if-no-files-found: error | |
| include-hidden-files: true | |
| overwrite: true | |
| - name: Push release branch | |
| if: ${{ !inputs.dry_run }} | |
| env: | |
| RELEASE_BRANCH: ${{ steps.branch.outputs.name }} | |
| run: git push --set-upstream origin "$RELEASE_BRANCH" | |
| - name: Create release pull request | |
| if: ${{ !inputs.dry_run }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_BRANCH: ${{ steps.branch.outputs.name }} | |
| PACKAGE_NAMES: ${{ steps.selector.outputs.names }} | |
| RELEASE_TYPE: ${{ inputs.release_type }} | |
| PREID: ${{ inputs.preid }} | |
| NPM_TAG: ${{ inputs.npm_tag }} | |
| run: | | |
| set -euo pipefail | |
| body_file="$RUNNER_TEMP/release-pr-body.md" | |
| { | |
| echo "## Release candidate" | |
| echo | |
| echo "This pull request was prepared by the package release workflow." | |
| echo | |
| echo "- Packages: \`$PACKAGE_NAMES\`" | |
| if [[ "$RELEASE_TYPE" == "prerelease" ]]; then | |
| echo "- Version bump: \`$RELEASE_TYPE\` (preid \`$PREID\`)" | |
| else | |
| echo "- Version bump: \`$RELEASE_TYPE\`" | |
| fi | |
| echo "- npm tag: \`$NPM_TAG\`" | |
| echo "- Build artifact: workflow run [$GITHUB_RUN_ID]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)" | |
| echo | |
| echo "The release workflow will create package tags and publish the validated artifact when this PR is merged into main." | |
| } > "$body_file" | |
| existing_pr_url=$(gh pr list \ | |
| --base main \ | |
| --head "$RELEASE_BRANCH" \ | |
| --state open \ | |
| --json url \ | |
| --jq '.[0].url') | |
| if [[ -n "$existing_pr_url" ]]; then | |
| echo "Release pull request already exists: $existing_pr_url" | |
| else | |
| gh pr create \ | |
| --base main \ | |
| --head "$RELEASE_BRANCH" \ | |
| --title "chore(release): prepare @ai-primitives-hub packages" \ | |
| --body-file "$body_file" | |
| fi | |
| publish: | |
| name: Tag and publish merged release | |
| if: >- | |
| ${{ github.event_name == 'pull_request' && | |
| github.event.pull_request.merged == true && | |
| github.event.pull_request.base.ref == 'main' && | |
| github.event.pull_request.head.repo.full_name == github.repository && | |
| startsWith(github.event.pull_request.head.ref, 'release/packages/') }} | |
| runs-on: ubuntu-latest | |
| environment: npmjs | |
| permissions: | |
| contents: write | |
| actions: read | |
| id-token: write | |
| steps: | |
| - name: Checkout merged release | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.event.pull_request.merge_commit_sha }} | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: pnpm | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: Determine release artifact | |
| id: release | |
| env: | |
| RELEASE_BRANCH: ${{ github.event.pull_request.head.ref }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$RELEASE_BRANCH" =~ ^release/packages/(all|core|infra|app|cli)/(latest|next|alpha|beta)/([0-9]+)$ ]]; then | |
| echo "The merged pull request is not a recognized package release branch: $RELEASE_BRANCH" >&2 | |
| exit 1 | |
| fi | |
| { | |
| echo "packages=${BASH_REMATCH[1]}" | |
| echo "npm_tag=${BASH_REMATCH[2]}" | |
| echo "artifact_run_id=${BASH_REMATCH[3]}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Download release build artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-packages | |
| github-token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| run-id: ${{ steps.release.outputs.artifact_run_id }} | |
| path: .release-artifacts | |
| - name: Verify release artifact | |
| id: artifact | |
| env: | |
| REQUESTED_PACKAGES: ${{ steps.release.outputs.packages }} | |
| REQUESTED_NPM_TAG: ${{ steps.release.outputs.npm_tag }} | |
| ARTIFACT_RUN_ID: ${{ steps.release.outputs.artifact_run_id }} | |
| RELEASE_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| node <<'NODE' | |
| const fs = require('node:fs'); | |
| const path = require('node:path'); | |
| const fail = (message) => { | |
| throw new Error(message); | |
| }; | |
| const requestedNames = process.env.REQUESTED_PACKAGES === 'all' | |
| ? ['core', 'infra', 'app', 'cli'] | |
| : [process.env.REQUESTED_PACKAGES]; | |
| const manifestPath = path.join('.release-artifacts', 'release-manifest.json'); | |
| if (!fs.existsSync(manifestPath)) { | |
| fail(`Release manifest is missing: ${manifestPath}`); | |
| } | |
| const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); | |
| if (manifest.formatVersion !== 1) fail('Unsupported release artifact format.'); | |
| if (manifest.runId !== Number(process.env.ARTIFACT_RUN_ID)) fail('Release artifact run does not match the release branch.'); | |
| if (manifest.npmTag !== process.env.REQUESTED_NPM_TAG) fail('Release artifact npm tag does not match the release branch.'); | |
| if (!/^[0-9a-f]{40}$/.test(manifest.sourceCommit)) fail('Release artifact source commit is invalid.'); | |
| if (manifest.sourceCommit !== process.env.RELEASE_HEAD_SHA) fail('Release artifact was not built from the merged pull request head.'); | |
| const actualNames = manifest.packages.map((entry) => entry.directory); | |
| if (actualNames.join('\n') !== requestedNames.join('\n')) fail('Release artifact package selection does not match the release branch.'); | |
| for (const entry of manifest.packages) { | |
| const packageJsonPath = path.join('packages', entry.directory, 'package.json'); | |
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); | |
| if (packageJson.name !== entry.name || packageJson.version !== entry.version) { | |
| fail(`Merged package metadata does not match the validated artifact: ${entry.name}`); | |
| } | |
| const distPath = path.join('.release-artifacts', 'packages', entry.directory, 'dist'); | |
| if (!fs.existsSync(distPath)) fail(`Build output is missing from the release artifact: ${distPath}`); | |
| fs.rmSync(path.join('packages', entry.directory, 'dist'), { recursive: true, force: true }); | |
| fs.cpSync(distPath, path.join('packages', entry.directory, 'dist'), { recursive: true }); | |
| } | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `package_names=${requestedNames.join(' ')}\n`); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `npm_tag=${process.env.REQUESTED_NPM_TAG}\n`); | |
| NODE | |
| - name: Install dependencies without lifecycle scripts | |
| run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts | |
| - name: Check npm versions before tagging | |
| env: | |
| PACKAGE_NAMES: ${{ steps.artifact.outputs.package_names }} | |
| MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} | |
| run: | | |
| set -euo pipefail | |
| for name in $PACKAGE_NAMES; do | |
| version=$(node -p "require('./packages/$name/package.json').version") | |
| if npm view "@ai-primitives-hub/$name@$version" version --registry=https://registry.npmjs.org >/dev/null 2>&1; then | |
| tag="packages/${name}-v${version}" | |
| if ! git show-ref --tags --verify --quiet "refs/tags/$tag" || [[ "$(git rev-list -n 1 "$tag")" != "$MERGE_SHA" ]]; then | |
| echo "@ai-primitives-hub/$name@$version already exists without the expected release tag." >&2 | |
| exit 1 | |
| fi | |
| echo "@ai-primitives-hub/$name@$version is already published by this release; retry will leave it unchanged." | |
| fi | |
| done | |
| - name: Create and push package tags | |
| env: | |
| PACKAGE_NAMES: ${{ steps.artifact.outputs.package_names }} | |
| MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| for name in $PACKAGE_NAMES; do | |
| version=$(node -p "require('./packages/$name/package.json').version") | |
| tag="packages/${name}-v${version}" | |
| if git show-ref --tags --verify --quiet "refs/tags/$tag"; then | |
| tag_sha=$(git rev-list -n 1 "$tag") | |
| if [[ "$tag_sha" != "$MERGE_SHA" ]]; then | |
| echo "Tag $tag already exists at $tag_sha, not $MERGE_SHA." >&2 | |
| exit 1 | |
| fi | |
| echo "Tag $tag already points at the merged release; leaving it unchanged." | |
| else | |
| git tag "$tag" "$MERGE_SHA" | |
| git push origin "refs/tags/$tag" | |
| fi | |
| done | |
| - name: Publish packages to npmjs | |
| env: | |
| PACKAGE_NAMES: ${{ steps.artifact.outputs.package_names }} | |
| NPM_TAG: ${{ steps.artifact.outputs.npm_tag }} | |
| run: | | |
| set -euo pipefail | |
| for name in $PACKAGE_NAMES; do | |
| echo "Publishing @ai-primitives-hub/$name ..." | |
| version=$(node -p "require('./packages/$name/package.json').version") | |
| if npm view "@ai-primitives-hub/$name@$version" version --registry=https://registry.npmjs.org >/dev/null 2>&1; then | |
| echo "@ai-primitives-hub/$name@$version is already published; skipping it for an idempotent retry." | |
| continue | |
| fi | |
| pnpm --filter "@ai-primitives-hub/$name" publish \ | |
| --access public \ | |
| --tag "$NPM_TAG" \ | |
| --no-git-checks \ | |
| --ignore-scripts \ | |
| --provenance | |
| done |