Skip to content

Release and Publish #19

Release and Publish

Release and Publish #19

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