Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 269 additions & 31 deletions .github/workflows/release-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,39 @@ name: Release and Publish
on:
workflow_dispatch:
inputs:
version:
description: 'Version to publish (optional - will use conventional commits if not specified)'
required: false
default: ''
prerelease:
description: 'Infer or coerce the release as a next prerelease'
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
Expand All @@ -26,14 +50,183 @@ concurrency:
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: |
node <<'NODE'
const fs = require('node:fs');

const planPath = process.env.RELEASE_PLAN_PATH;
const notesPath = process.env.RELEASE_NOTES_PATH;
const lines = [
'## Release Validation Summary',
'',
`- Mode: ${process.env.RELEASE_MODE}`,
`- Outcome: ${process.env.RELEASE_OUTCOME}`,
`- Release intent input: ${process.env.RELEASE_INTENT}`,
`- Bump input: ${process.env.RELEASE_BUMP}`,
`- Allow major without prerelease: ${process.env.RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE}`,
`- Manual version input: ${process.env.RELEASE_MANUAL_VERSION || 'none'}`,
];

if (planPath && fs.existsSync(planPath)) {
const plan = JSON.parse(fs.readFileSync(planPath, 'utf8'));

lines.push(
`- Package: ${plan.packageName}`,
`- Release intent: ${plan.releaseIntent}`,
`- Release bump: ${plan.releaseBump}`,
`- Planned version: ${plan.nextVersion}`,
`- Release tag: ${plan.releaseTag}`,
`- npm dist-tag: ${plan.npmDistTag}`,
`- Head SHA: ${plan.headSha || 'unknown'}`,
`- Commit count: ${plan.commitCount}`,
`- Release notes base tag: ${plan.releaseNotesBaseTag || 'none'}`,
`- Release note commit count: ${plan.releaseNotesCommitCount}`,
);
} else {
lines.push('- Planned version: unavailable; release planning did not complete.');
}

if (notesPath && fs.existsSync(notesPath)) {
const notes = fs.readFileSync(notesPath, 'utf8').trimEnd();

lines.push(
'',
'## Future GitHub Release Notes',
'',
notes || '_No release notes were generated._',
);
}

fs.writeFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join('\n')}\n`);
NODE

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: npm-release
environment:
name: npm-release
url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -70,54 +263,94 @@ jobs:
echo "GitHub Actions OIDC request environment: available"

- name: Configure release paths
run: echo "RELEASE_PLAN_PATH=$RUNNER_TEMP/release-plan.json" >> "$GITHUB_ENV"
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: Run tests
run: pnpm turbo run test
- name: Restore validated release plan
env:
VALIDATED_RELEASE_PLAN: ${{ needs.validate-release.outputs.release-plan }}
run: |
node <<'NODE'
const fs = require('node:fs');

- name: Check for lint errors
run: pnpm turbo run lint --filter=@limitless-angular/sanity
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 "actions@github.com"

- name: Validate and publish release
- 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 }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_PRERELEASE: ${{ inputs.prerelease }}
run: |
PLAN_ARGS=()
PIPELINE_ARGS=()
if [ -n "$RELEASE_VERSION" ]; then
PLAN_ARGS+=(--version "$RELEASE_VERSION")
PIPELINE_ARGS+=(--version "$RELEASE_VERSION")
PIPELINE_ARGS=(--intent "$RELEASE_INTENT" --bump "$RELEASE_BUMP")
if [ "$RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE" = "true" ]; then
PIPELINE_ARGS+=(--allow-major-without-prerelease)
fi
if [ "$RELEASE_PRERELEASE" = "true" ]; then
PLAN_ARGS+=(--prerelease)
PIPELINE_ARGS+=(--prerelease)
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 --filter=@limitless-angular/release-tools run --silent release:plan "${PLAN_ARGS[@]}" --json > "$RELEASE_PLAN_PATH"
cat "$RELEASE_PLAN_PATH"

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 }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_PRERELEASE: ${{ inputs.prerelease }}
RELEASE_OUTCOME: ${{ steps.release.outcome || 'skipped' }}
run: |
node <<'NODE'
const fs = require('node:fs');
Expand All @@ -128,18 +361,23 @@ jobs:
'',
`- Mode: ${process.env.RELEASE_MODE}`,
`- Outcome: ${process.env.RELEASE_OUTCOME}`,
`- Version input: ${process.env.RELEASE_VERSION || 'conventional commits'}`,
`- Prerelease input: ${process.env.RELEASE_PRERELEASE}`,
`- Release intent input: ${process.env.RELEASE_INTENT}`,
`- Bump input: ${process.env.RELEASE_BUMP}`,
`- Allow major without prerelease: ${process.env.RELEASE_ALLOW_MAJOR_WITHOUT_PRERELEASE}`,
`- Manual version input: ${process.env.RELEASE_MANUAL_VERSION || 'none'}`,
];

if (planPath && fs.existsSync(planPath)) {
const plan = JSON.parse(fs.readFileSync(planPath, 'utf8'));

lines.push(
`- Package: ${plan.packageName}`,
`- Release intent: ${plan.releaseIntent}`,
`- Release bump: ${plan.releaseBump}`,
`- Planned version: ${plan.nextVersion}`,
`- Release tag: ${plan.releaseTag}`,
`- npm dist-tag: ${plan.npmDistTag}`,
`- Head SHA: ${plan.headSha || 'unknown'}`,
`- Commit count: ${plan.commitCount}`,
`- Release notes base tag: ${plan.releaseNotesBaseTag || 'none'}`,
`- Release note commit count: ${plan.releaseNotesCommitCount}`,
Expand Down
Loading
Loading