Release and Publish #19
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 and Publish | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| release_intent: | |
| description: 'Release intent to publish' | |
| required: true | |
| default: stable | |
| type: choice | |
| options: | |
| - stable | |
| - prerelease | |
| - promote-stable | |
| - manual | |
| bump: | |
| description: 'Version bump for stable or prerelease intents' | |
| required: true | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - patch | |
| - minor | |
| - major | |
| allow_major_without_prerelease: | |
| description: 'Allow a stable major release without a prerelease train' | |
| required: false | |
| default: false | |
| type: boolean | |
| manual_version: | |
| description: 'Exact semver version when release_intent is manual' | |
| required: false | |
| default: '' | |
| manual_reason: | |
| description: 'Reason when release_intent is manual' | |
| required: false | |
| default: '' | |
| verbose: | |
| description: 'Enable verbose logging' | |
| required: false | |
| default: false | |
| type: boolean | |
| env: | |
| TURBO_TELEMETRY_DISABLED: '1' | |
| concurrency: | |
| group: release-publish-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| validate-release: | |
| if: github.repository == 'limitless-angular/limitless-angular' && github.ref == 'refs/heads/main' | |
| name: Validate Release Plan | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: read | |
| outputs: | |
| release-plan: ${{ steps.plan.outputs.release-plan }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - uses: pnpm/action-setup@v6 | |
| name: Install pnpm | |
| with: | |
| run_install: false | |
| - name: Install Node.js per package.json | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: 'pnpm' | |
| registry-url: https://registry.npmjs.org/ | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile --prefer-offline | |
| - name: Verify environment | |
| run: | | |
| echo "Node version: $(node -v)" | |
| echo "pnpm version: $(pnpm -v)" | |
| echo "Workspace directory: $(pwd)" | |
| - name: Configure release paths | |
| run: | | |
| echo "RELEASE_PLAN_PATH=$RUNNER_TEMP/release-plan.json" >> "$GITHUB_ENV" | |
| echo "RELEASE_NOTES_PATH=$RUNNER_TEMP/release-notes.md" >> "$GITHUB_ENV" | |
| - name: Compute release plan | |
| id: plan | |
| env: | |
| RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE: ${{ inputs.allow_major_without_prerelease }} | |
| RELEASE_BUMP: ${{ inputs.bump }} | |
| RELEASE_INTENT: ${{ inputs.release_intent }} | |
| RELEASE_MANUAL_REASON: ${{ inputs.manual_reason }} | |
| RELEASE_MANUAL_VERSION: ${{ inputs.manual_version }} | |
| run: | | |
| PLAN_ARGS=(--intent "$RELEASE_INTENT" --bump "$RELEASE_BUMP") | |
| if [ "$RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE" = "true" ]; then | |
| PLAN_ARGS+=(--allow-major-without-prerelease) | |
| fi | |
| if [ -n "$RELEASE_MANUAL_VERSION" ]; then | |
| PLAN_ARGS+=(--manual-version "$RELEASE_MANUAL_VERSION") | |
| fi | |
| if [ -n "$RELEASE_MANUAL_REASON" ]; then | |
| PLAN_ARGS+=(--manual-reason "$RELEASE_MANUAL_REASON") | |
| fi | |
| pnpm --filter=@limitless-angular/release-tools run --silent release:plan "${PLAN_ARGS[@]}" --json > "$RELEASE_PLAN_PATH" | |
| cat "$RELEASE_PLAN_PATH" | |
| pnpm --filter=@limitless-angular/release-tools run --silent release:notes "${PLAN_ARGS[@]}" > "$RELEASE_NOTES_PATH" | |
| cat "$RELEASE_NOTES_PATH" | |
| node <<'NODE' | |
| const fs = require('node:fs'); | |
| const plan = JSON.parse(fs.readFileSync(process.env.RELEASE_PLAN_PATH, 'utf8')); | |
| fs.appendFileSync( | |
| process.env.GITHUB_OUTPUT, | |
| `release-plan<<RELEASE_PLAN_JSON\n${JSON.stringify(plan)}\nRELEASE_PLAN_JSON\n`, | |
| ); | |
| NODE | |
| - name: Run tests | |
| run: pnpm turbo run test | |
| - name: Check for lint errors | |
| run: pnpm turbo run lint --filter=@limitless-angular/sanity | |
| - name: Validate release dry run | |
| id: release | |
| env: | |
| RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE: ${{ inputs.allow_major_without_prerelease }} | |
| RELEASE_BUMP: ${{ inputs.bump }} | |
| RELEASE_INTENT: ${{ inputs.release_intent }} | |
| RELEASE_MANUAL_REASON: ${{ inputs.manual_reason }} | |
| RELEASE_MANUAL_VERSION: ${{ inputs.manual_version }} | |
| RELEASE_VERBOSE: ${{ inputs.verbose }} | |
| run: | | |
| PIPELINE_ARGS=(--intent "$RELEASE_INTENT" --bump "$RELEASE_BUMP") | |
| if [ "$RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE" = "true" ]; then | |
| PIPELINE_ARGS+=(--allow-major-without-prerelease) | |
| fi | |
| if [ -n "$RELEASE_MANUAL_VERSION" ]; then | |
| PIPELINE_ARGS+=(--manual-version "$RELEASE_MANUAL_VERSION") | |
| fi | |
| if [ -n "$RELEASE_MANUAL_REASON" ]; then | |
| PIPELINE_ARGS+=(--manual-reason "$RELEASE_MANUAL_REASON") | |
| fi | |
| if [ "$RELEASE_VERBOSE" = "true" ]; then | |
| PIPELINE_ARGS+=(--verbose) | |
| fi | |
| pnpm turbo run release:dry-run --filter=@limitless-angular/release-tools -- "${PIPELINE_ARGS[@]}" | |
| - name: Generate release validation summary | |
| if: always() | |
| env: | |
| RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE: ${{ inputs.allow_major_without_prerelease }} | |
| RELEASE_BUMP: ${{ inputs.bump }} | |
| RELEASE_INTENT: ${{ inputs.release_intent }} | |
| RELEASE_MANUAL_VERSION: ${{ inputs.manual_version }} | |
| RELEASE_MODE: validation | |
| RELEASE_OUTCOME: ${{ steps.release.outcome || 'skipped' }} | |
| run: pnpm --filter=@limitless-angular/release-tools run --silent release:summary | |
| release-and-publish: | |
| needs: validate-release | |
| if: github.repository == 'limitless-angular/limitless-angular' && github.ref == 'refs/heads/main' | |
| name: Release and Publish to npm | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: write | |
| id-token: write | |
| environment: | |
| name: npm-release | |
| url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - uses: pnpm/action-setup@v6 | |
| name: Install pnpm | |
| with: | |
| run_install: false | |
| - name: Install Node.js per package.json | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: 'pnpm' | |
| - name: Install npm for trusted publishing | |
| run: npm install -g npm@^11.10.0 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile --prefer-offline | |
| - name: Verify environment | |
| run: | | |
| echo "Node version: $(node -v)" | |
| echo "npm version: $(npm -v)" | |
| echo "pnpm version: $(pnpm -v)" | |
| echo "Workspace directory: $(pwd)" | |
| if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]; then | |
| echo "::error::GitHub Actions OIDC request environment is missing; verify permissions.id-token: write." | |
| exit 1 | |
| fi | |
| echo "GitHub Actions OIDC request environment: available" | |
| - name: Configure release paths | |
| run: | | |
| echo "RELEASE_PLAN_PATH=$RUNNER_TEMP/release-plan.json" >> "$GITHUB_ENV" | |
| echo "VALIDATED_RELEASE_PLAN_PATH=$RUNNER_TEMP/validated-release-plan.json" >> "$GITHUB_ENV" | |
| - name: Restore validated release plan | |
| env: | |
| VALIDATED_RELEASE_PLAN: ${{ needs.validate-release.outputs.release-plan }} | |
| run: | | |
| node <<'NODE' | |
| const fs = require('node:fs'); | |
| if (!process.env.VALIDATED_RELEASE_PLAN) { | |
| throw new Error('Missing validated release plan from validation job.'); | |
| } | |
| fs.writeFileSync( | |
| process.env.VALIDATED_RELEASE_PLAN_PATH, | |
| `${process.env.VALIDATED_RELEASE_PLAN}\n`, | |
| ); | |
| NODE | |
| - name: Verify release plan did not change | |
| id: verify | |
| env: | |
| RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE: ${{ inputs.allow_major_without_prerelease }} | |
| RELEASE_BUMP: ${{ inputs.bump }} | |
| RELEASE_INTENT: ${{ inputs.release_intent }} | |
| RELEASE_MANUAL_REASON: ${{ inputs.manual_reason }} | |
| RELEASE_MANUAL_VERSION: ${{ inputs.manual_version }} | |
| run: | | |
| PLAN_ARGS=(--intent "$RELEASE_INTENT" --bump "$RELEASE_BUMP") | |
| if [ "$RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE" = "true" ]; then | |
| PLAN_ARGS+=(--allow-major-without-prerelease) | |
| fi | |
| if [ -n "$RELEASE_MANUAL_VERSION" ]; then | |
| PLAN_ARGS+=(--manual-version "$RELEASE_MANUAL_VERSION") | |
| fi | |
| if [ -n "$RELEASE_MANUAL_REASON" ]; then | |
| PLAN_ARGS+=(--manual-reason "$RELEASE_MANUAL_REASON") | |
| fi | |
| pnpm --filter=@limitless-angular/release-tools run --silent release:plan "${PLAN_ARGS[@]}" --json > "$RELEASE_PLAN_PATH" | |
| cat "$RELEASE_PLAN_PATH" | |
| pnpm --filter=@limitless-angular/release-tools run --silent release:verify-plan --expected "$VALIDATED_RELEASE_PLAN_PATH" --actual "$RELEASE_PLAN_PATH" | |
| - name: Set up Git user | |
| run: | | |
| git config --global user.name "GitHub Actions" | |
| git config --global user.email "[email protected]" | |
| - name: Publish release | |
| id: release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: true | |
| RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE: ${{ inputs.allow_major_without_prerelease }} | |
| RELEASE_BUMP: ${{ inputs.bump }} | |
| RELEASE_INTENT: ${{ inputs.release_intent }} | |
| RELEASE_MANUAL_REASON: ${{ inputs.manual_reason }} | |
| RELEASE_MANUAL_VERSION: ${{ inputs.manual_version }} | |
| RELEASE_VERBOSE: ${{ inputs.verbose }} | |
| run: | | |
| PIPELINE_ARGS=(--intent "$RELEASE_INTENT" --bump "$RELEASE_BUMP") | |
| if [ "$RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE" = "true" ]; then | |
| PIPELINE_ARGS+=(--allow-major-without-prerelease) | |
| fi | |
| if [ -n "$RELEASE_MANUAL_VERSION" ]; then | |
| PIPELINE_ARGS+=(--manual-version "$RELEASE_MANUAL_VERSION") | |
| fi | |
| if [ -n "$RELEASE_MANUAL_REASON" ]; then | |
| PIPELINE_ARGS+=(--manual-reason "$RELEASE_MANUAL_REASON") | |
| fi | |
| if [ "$RELEASE_VERBOSE" = "true" ]; then | |
| PIPELINE_ARGS+=(--verbose) | |
| fi | |
| pnpm turbo run release:publish --filter=@limitless-angular/release-tools -- "${PIPELINE_ARGS[@]}" | |
| - name: Generate release summary | |
| if: always() | |
| env: | |
| RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE: ${{ inputs.allow_major_without_prerelease }} | |
| RELEASE_BUMP: ${{ inputs.bump }} | |
| RELEASE_INTENT: ${{ inputs.release_intent }} | |
| RELEASE_MANUAL_VERSION: ${{ inputs.manual_version }} | |
| RELEASE_MODE: publish | |
| RELEASE_OUTCOME: ${{ steps.release.outcome || 'skipped' }} | |
| run: pnpm --filter=@limitless-angular/release-tools run --silent release:summary |