From 096df5bd8edeb43bbb8d463196a26a98d18e28cc Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Fri, 6 Feb 2026 12:47:48 -0800 Subject: [PATCH 1/2] fix: release plan workflow --- .github/workflows/plan-release.yml | 76 +++++++++--------------------- .github/workflows/publish.yml | 49 ++++++------------- RELEASE.md | 16 +++---- 3 files changed, 44 insertions(+), 97 deletions(-) diff --git a/.github/workflows/plan-release.yml b/.github/workflows/plan-release.yml index 2e2c301..5f003f8 100644 --- a/.github/workflows/plan-release.yml +++ b/.github/workflows/plan-release.yml @@ -1,90 +1,58 @@ -name: Release Plan Review +name: Plan Release on: + workflow_dispatch: push: branches: - main - master - pull_request: + pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ types: - labeled + - unlabeled concurrency: group: plan-release # only the latest one of these should ever be running cancel-in-progress: true jobs: - check-plan: - name: "Check Release Plan" + should-run-release-plan-prepare: + name: Should we run release-plan prepare? runs-on: ubuntu-latest outputs: - command: ${{ steps.check-release.outputs.command }} - + should-prepare: ${{ steps.should-prepare.outputs.should-prepare }} steps: - - uses: actions/checkout@v4 + - uses: release-plan/actions/should-prepare-release@v1 with: - fetch-depth: 0 ref: 'master' - # This will only cause the `check-plan` job to have a "command" of `release` - # when the .release-plan.json file was changed on the last commit. - - id: check-release - run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT + id: should-prepare - prepare_release_notes: - name: Prepare Release Notes + create-prepare-release-pr: + name: Create Prepare Release PR runs-on: ubuntu-latest timeout-minutes: 5 - needs: check-plan + needs: should-run-release-plan-prepare permissions: contents: write + issues: read pull-requests: write - outputs: - explanation: ${{ steps.explanation.outputs.text }} - # only run on push event if plan wasn't updated (don't create a release plan when we're releasing) - # only run on labeled event if the PR has already been merged - if: (github.event_name == 'push' && needs.check-plan.outputs.command != 'release') || (github.event_name == 'pull_request' && github.event.pull_request.merged == true) - + if: needs.should-run-release-plan-prepare.outputs.should-prepare == 'true' steps: - - uses: actions/checkout@v4 - # We need to download lots of history so that - # github-changelog can discover what's changed since the last release + - uses: release-plan/actions/prepare@v1 + name: Run release-plan prepare with: - fetch-depth: 0 ref: 'master' - - uses: actions/setup-node@v4 - with: - node-version: 18 - - - uses: pnpm/action-setup@v3 - with: - version: 8 - - run: pnpm install --frozen-lockfile - - - name: "Generate Explanation and Prep Changelogs" - id: explanation - run: | - set +e - - pnpm release-plan prepare 2> >(tee -a stderr.log >&2) - - - if [ $? -ne 0 ]; then - echo 'text<> $GITHUB_OUTPUT - cat stderr.log >> $GITHUB_OUTPUT - echo 'EOF' >> $GITHUB_OUTPUT - else - echo 'text<> $GITHUB_OUTPUT - jq .description .release-plan.json -r >> $GITHUB_OUTPUT - echo 'EOF' >> $GITHUB_OUTPUT - fi env: GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} + id: explanation - - uses: peter-evans/create-pull-request@v6 + - uses: peter-evans/create-pull-request@v8 + name: Create Prepare Release PR with: - commit-message: "Prepare Release using 'release-plan'" + commit-message: "Prepare Release ${{ steps.explanation.outputs.new-version}} using 'release-plan'" labels: "internal" + sign-commits: true branch: release-preview - title: Prepare Release + title: Prepare Release ${{ steps.explanation.outputs.new-version }} body: | This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e7fb1e5..531161e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,5 @@ -# For every push to the master branch, this checks if the release-plan was -# updated and if it was it will publish stable npm packages based on the -# release plan +# For every push to the primary branch with .release-plan.json modified, +# runs release-plan. name: Publish Stable @@ -10,53 +9,33 @@ on: branches: - main - master + paths: + - '.release-plan.json' concurrency: group: publish-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: - check-plan: - name: "Check Release Plan" - runs-on: ubuntu-latest - outputs: - command: ${{ steps.check-release.outputs.command }} - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: 'master' - # This will only cause the `check-plan` job to have a result of `success` - # when the .release-plan.json file was changed on the last commit. This - # plus the fact that this action only runs on main will be enough of a guard - - id: check-release - run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT - publish: name: "NPM Publish" runs-on: ubuntu-latest - needs: check-plan - if: needs.check-plan.outputs.command == 'release' permissions: contents: write - pull-requests: write + id-token: write + attestations: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v6 with: - node-version: 18 - # This creates an .npmrc that reads the NODE_AUTH_TOKEN environment variable + node-version: 22 registry-url: 'https://registry.npmjs.org' - - - uses: pnpm/action-setup@v3 - with: - version: 8 + cache: pnpm + - run: npm install -g npm@latest # ensure that the globally installed npm is new enough to support OIDC - run: pnpm install --frozen-lockfile - - name: npm publish - run: pnpm release-plan publish - + - name: Publish to NPM + run: NPM_CONFIG_PROVENANCE=true pnpm release-plan publish env: GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/RELEASE.md b/RELEASE.md index 252b910..2b8638b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -4,21 +4,21 @@ Releases in this repo are mostly automated using [release-plan](https://github.c ## Preparation -Since the majority of the actual release process is automated, the remaining tasks before releasing are: +Since the majority of the actual release process is automated, the remaining tasks before releasing are: -- correctly labeling **all** pull requests that have been merged since the last release -- updating pull request titles so they make sense to our users +- correctly labeling **all** pull requests that have been merged since the last release +- updating pull request titles so they make sense to our users Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall guiding principle here is that changelogs are for humans, not machines. When reviewing merged PR's the labels to be used are: -* breaking - Used when the PR is considered a breaking change. -* enhancement - Used when the PR adds a new feature or enhancement. -* bug - Used when the PR fixes a bug included in a previous release. -* documentation - Used when the PR adds or updates documentation. -* internal - Internal changes or things that don't fit in any other category. +- breaking - Used when the PR is considered a breaking change. +- enhancement - Used when the PR adds a new feature or enhancement. +- bug - Used when the PR fixes a bug included in a previous release. +- documentation - Used when the PR adds or updates documentation. +- internal - Internal changes or things that don't fit in any other category. **Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal` From 2b005b862f1a189b253805b8d15e1763c96d8aba Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Fri, 6 Feb 2026 14:09:40 -0800 Subject: [PATCH 2/2] feat: make FlashMessage component generic for type-safe custom fields --- README.md | 53 +++++++++++++++++++ UPGRADING.md | 17 ++++++ demo-app/components/demo-examples.gts | 51 ++++++++++++++---- demo-app/components/flash-container.gts | 12 ++++- demo-app/services/flash-messages.ts | 24 +++++++++ package.json | 6 +-- pnpm-lock.yaml | 20 +++---- src/components/flash-message.gts | 23 ++++++-- src/flash/object.ts | 4 +- .../components/flash-message-test.gts | 29 ++++++++++ 10 files changed, 208 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index d9d56be..82183e6 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ This ember addon adds a flash message service and component to your app. - [TypeScript](#typescript) - [Basic Usage](#basic-usage) - [Custom Fields with Generics](#custom-fields-with-generics) + - [Typing Dynamically Registered Methods](#typing-dynamically-registered-methods) - [Displaying flash messages](#displaying-flash-messages) - [Custom `close` action](#custom-close-action) - [Styling with Foundation or Bootstrap](#styling-with-foundation-or-bootstrap) @@ -499,6 +500,58 @@ this.flashMessages.success('Oops', { }); ``` +Custom fields are also type-safe in templates. The `FlashMessage` component exposes the properly typed flash object with your custom fields: + +```gjs +import { FlashMessage } from 'ember-cli-flash'; + + +``` + +### Typing Dynamically Registered Methods + +When you configure custom `types` in `flashMessageDefaults`, the service dynamically creates convenience methods for each type at runtime. The base class already declares types for the default methods (`success`, `info`, `warning`, `danger`, `alert`, `secondary`), but TypeScript doesn't automatically recognize any custom types you add. + +To get type safety for custom type methods, declare them explicitly in your service subclass: + +```typescript +import { FlashMessagesService } from 'ember-cli-flash'; +import type { FlashObjectOptions } from 'ember-cli-flash'; + +interface CustomFlashFields { + id?: string; + category?: string; +} + +type Options = FlashObjectOptions & CustomFlashFields; + +export default class MyFlashMessages extends FlashMessagesService { + // Only declare custom types not in the base class + // (success, info, warning, danger, alert, secondary are already typed) + declare error: (message: string, options?: Options) => this; + declare custom: (message: string, options?: Options) => this; + + get flashMessageDefaults() { + return { + ...super.flashMessageDefaults, + types: ['error', 'success', 'warning', 'custom'], + }; + } +} +``` + +This pattern uses TypeScript's `declare` keyword to inform the type system about methods that exist at runtime but aren't defined in the base class types. + ## Displaying flash messages Then, to display somewhere in your app, add this to your component: diff --git a/UPGRADING.md b/UPGRADING.md index 5a64686..8ff5315 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -132,8 +132,14 @@ Register custom types for your application: ```typescript // app/services/flash-messages.ts import { FlashMessagesService } from 'ember-cli-flash'; +import type { FlashObjectOptions } from 'ember-cli-flash'; export default class FlashMessages extends FlashMessagesService { + // Declare custom types for TypeScript (base types like success, warning are already typed) + declare notice: (message: string, options?: FlashObjectOptions) => this; + declare error: (message: string, options?: FlashObjectOptions) => this; + declare system: (message: string, options?: FlashObjectOptions) => this; + get flashMessageDefaults() { return { ...super.flashMessageDefaults, @@ -247,6 +253,17 @@ const flash = this.flashMessages.findBy('id', 'save-notification'); this.flashMessages.removeBy('userId', 123); ``` +The `FlashMessage` component is also generic and infers the type from the `@flash` arg, giving you type-safe access to custom fields in templates: + +```gjs +{{#each this.flashMessages.queue as |flash|}} + + {{flash.message}} + {{flash.actionUrl}} {{! ✓ Typed as string | undefined }} + +{{/each}} +``` + ### New Methods: `findBy` and `removeBy` Two new methods have been added to the service for finding and removing flash messages by any field: diff --git a/demo-app/components/demo-examples.gts b/demo-app/components/demo-examples.gts index 74c6dfd..84807cd 100644 --- a/demo-app/components/demo-examples.gts +++ b/demo-app/components/demo-examples.gts @@ -76,6 +76,19 @@ export default class DemoExamples extends Component { }); }; + // Custom typed methods (declared in service) + showError = () => { + this.flashMessages.error('Something went wrong!', { + category: 'system', + }); + }; + + showNotice = () => { + this.flashMessages.notice('Did you know? This is a custom notice type.', { + timeout: 5000, + }); + }; + // Custom fields with generics showWithCustomFields = () => { this.flashMessages.success('Message with custom fields', { @@ -319,17 +332,35 @@ export default class DemoExamples extends Component {

Use add() - with any Bootstrap alert type or your own custom types + with any Bootstrap alert type, or declare custom types in your service

- -
this.flashMessages.add({ message: 'Custom type message', type:
-          'secondary', timeout: 4000, });
+
+ + + +
+
// Custom types declared in service: // declare error:
+          (message: string, options?: Options) => this;
+          this.flashMessages.error('Something went wrong!');
+          this.flashMessages.notice('Did you know?');
{{! TypeScript Generics }} diff --git a/demo-app/components/flash-container.gts b/demo-app/components/flash-container.gts index 8f93fed..0dd3406 100644 --- a/demo-app/components/flash-container.gts +++ b/demo-app/components/flash-container.gts @@ -16,7 +16,17 @@ export default class FlashContainer extends Component {
{{component.flashType}} -

{{flashData.message}}

+ {{#if flashData.category}} + + {{flashData.category}} + + {{/if}} +

+ {{flashData.message}} +

+ {{#if flashData.id}} + ID: {{flashData.id}} + {{/if}}