Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
13 changes: 13 additions & 0 deletions .github/actions/build-notification/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.DS_Store
.idea
*.log
tmp/

*.tern-port
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.tsbuildinfo
.npm
.eslintcache
2 changes: 2 additions & 0 deletions .github/actions/build-notification/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
node_modules/
15 changes: 15 additions & 0 deletions .github/actions/build-notification/.prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict'

module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
jsxSingleQuote: false,
trailingComma: 'es5',
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'avoid',
endOfLine: 'lf',
}
63 changes: 63 additions & 0 deletions .github/actions/build-notification/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Build Notification

Single entrypoint for oe-core build Slack notifications. Workflows pass all context once at the end; this action decides whether to send, the payload shape, and which webhook to use.

## Usage

```yaml
notify-build:
name: Notify Build Outcome
runs-on: ubuntu-latest
needs: [decide-refs, run-build]
if: always()
steps:
- name: Fetch initial sources for action
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
submodules: false
path: ./oe-core-for-workflow
- name: Notify build outcome
uses: ./oe-core-for-workflow/.github/actions/build-notification
continue-on-error: true
with:
workflow: flex-build
event_name: ${{ github.event_name }}
job_result: ${{ needs.run-build.result }}
webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook_url_tagged: ${{ vars.SLACK_WEBHOOK_URL_TAGGED }}
workflow_name: Build Flex image on github workflows
oe_core_ref: ${{ needs.decide-refs.outputs.oe-core }}
monorepo_ref: ${{ needs.decide-refs.outputs.monorepo }}
firmware_ref: ${{ needs.decide-refs.outputs.ot3-firmware }}
variant: ${{ needs.decide-refs.outputs.variant }}
console_url: ${{ needs.run-build.outputs.console_url }}
system_url: ${{ needs.run-build.outputs.system_url }}
fullimage_url: ${{ needs.run-build.outputs.fullimage_url }}
version_file_url: ${{ needs.run-build.outputs.version_file_url }}
release_notes_file_url: ${{ needs.run-build.outputs.release_notes_file_url }}
```

The workflow job should always run (`if: always()`). All filtering happens inside the action.

## Routing rules (`flex-build`)

| Condition | Notification | Webhook |
| --- | --- | --- |
| `event_name != workflow_dispatch` | skip | |
| `job_result == success` and branch build | send `deployed` | `webhook_url` |
| `job_result == success` and tagged build with non-empty `variant` | send `deployed` | `webhook_url_tagged` (fallback: `webhook_url`) |
| `job_result == success` and tagged build with empty `variant` | skip | |
| `job_result == failure` | send `failure` | tagged or default by ref |
| `job_result == cancelled` | send `cancelled` | tagged or default by ref |
| other job results | skip | |

Payload shape varies by notification kind (`headline`, `status`, artifact URLs, refs, `tag` field).

## Outputs

| Output | Description |
| --- | --- |
| `sent` | `true` when a webhook POST was made |
| `notification_kind` | `deployed`, `failure`, `cancelled`, or `none` |
| `webhook_target` | `tagged`, `default`, or `none` |
| `skip_reason` | Why a notification was skipped |
85 changes: 85 additions & 0 deletions .github/actions/build-notification/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: "Build Notification"
description: "Single entrypoint for Flex build Slack notifications with routing rules."
inputs:
workflow:
description: "Workflow rule set to apply (currently flex-build)."
required: true
default: "flex-build"
event_name:
description: "GitHub event name (for example workflow_dispatch)."
required: true
job_result:
description: "Job result: success, failure, cancelled, or skipped."
required: true
webhook_url:
description: "Slack incoming webhook for branch builds and fallback."
required: false
default: ""
webhook_url_tagged:
description: "Slack incoming webhook for tagged builds. Falls back to webhook_url when empty."
required: false
default: ""
workflow_name:
description: "Human-readable workflow name for the Slack payload."
required: false
default: ""
workflow_run_url:
description: "Link to the workflow run. Defaults to the current run URL."
required: false
default: ""
failed_jobs:
description: "Comma-separated failed job names (optional)."
required: false
default: ""
oe_core_ref:
description: "oe-core ref."
required: false
default: ""
monorepo_ref:
description: "Monorepo ref."
required: false
default: ""
firmware_ref:
description: "Firmware ref."
required: false
default: ""
variant:
description: "Build variant from decide-refs (internal-release, release, or empty)."
required: false
default: ""
console_url:
description: "S3 console URL."
required: false
default: ""
system_url:
description: "System update zip URL."
required: false
default: ""
fullimage_url:
description: "Full image tar URL."
required: false
default: ""
version_file_url:
description: "Version file URL."
required: false
default: ""
release_notes_file_url:
description: "Release notes file URL."
required: false
default: ""
dry_run:
description: "When true, evaluate routing and build payload but do not POST."
required: false
default: "false"
outputs:
sent:
description: "true when a Slack notification was sent, false when skipped."
notification_kind:
description: "deployed, failure, cancelled, or none."
webhook_target:
description: "tagged, default, or none."
skip_reason:
description: "Why the notification was skipped, empty when sent."
runs:
using: "node24"
main: "dist/index.js"
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import {
buildSlackPayload,
decideNotification,
refType,
resolveWebhookUrl,
selectWebhookTarget,
stripRefPrefix,
tagField,
} from '../notification-rules'

describe('stripRefPrefix', () => {
test('removes refs/heads prefix', () => {
expect(stripRefPrefix('refs/heads/edge')).toBe('edge')
})

test('removes refs/tags prefix', () => {
expect(stripRefPrefix('refs/tags/v7.5.0')).toBe('v7.5.0')
})
})

describe('refType and tagField', () => {
test('identifies tag builds', () => {
expect(refType('refs/tags/v7.5.0')).toBe('tag')
expect(tagField('refs/tags/v7.5.0')).toBe('v7.5.0')
})

test('identifies branch builds', () => {
expect(refType('refs/heads/edge')).toBe('branch')
expect(tagField('refs/heads/edge')).toBe('None')
})
})

describe('decideNotification for flex-build', () => {
const base = {
workflow: 'flex-build' as const,
eventName: 'workflow_dispatch',
monorepoRef: 'refs/heads/edge',
variant: 'internal-release',
}

test('sends deployed for successful branch builds', () => {
expect(decideNotification({ ...base, jobResult: 'success' })).toMatchObject(
{
send: true,
kind: 'deployed',
webhookTarget: 'default',
statusLabel: 'deployed',
}
)
})

test('sends deployed for successful tagged builds with variant', () => {
expect(
decideNotification({
...base,
jobResult: 'success',
monorepoRef: 'refs/tags/v7.5.0',
variant: 'release',
})
).toMatchObject({
send: true,
kind: 'deployed',
webhookTarget: 'tagged',
})
})

test('skips deployed for tagged builds without variant', () => {
expect(
decideNotification({
...base,
jobResult: 'success',
monorepoRef: 'refs/tags/v7.5.0',
variant: '',
})
).toMatchObject({ send: false, kind: null })
})

test('sends failure and cancelled notifications', () => {
expect(decideNotification({ ...base, jobResult: 'failure' })).toMatchObject(
{ send: true, kind: 'failure', webhookTarget: 'default' }
)
expect(
decideNotification({ ...base, jobResult: 'cancelled' })
).toMatchObject({ send: true, kind: 'cancelled', webhookTarget: 'default' })
})

test('skips non workflow_dispatch events', () => {
expect(
decideNotification({ ...base, eventName: 'push', jobResult: 'success' })
).toMatchObject({ send: false })
})

test('skips skipped job results', () => {
expect(decideNotification({ ...base, jobResult: 'skipped' })).toMatchObject(
{ send: false }
)
})
})

describe('webhook destination routing', () => {
test('selects tagged vs default webhook targets', () => {
expect(selectWebhookTarget('refs/tags/v7.5.0')).toBe('tagged')
expect(selectWebhookTarget('refs/heads/edge')).toBe('default')
})

test('resolves tagged webhook with fallback', () => {
expect(
resolveWebhookUrl('tagged', 'https://default', 'https://tagged')
).toBe('https://tagged')
expect(resolveWebhookUrl('tagged', 'https://default', '')).toBe(
'https://default'
)
expect(
resolveWebhookUrl('default', 'https://default', 'https://tagged')
).toBe('https://default')
})
})

describe('buildSlackPayload', () => {
test('builds the Slack workflow payload', () => {
const decision = decideNotification({
workflow: 'flex-build',
eventName: 'workflow_dispatch',
jobResult: 'success',
monorepoRef: 'refs/heads/edge',
variant: 'internal-release',
})

expect(
buildSlackPayload({
workflow: 'flex-build',
eventName: 'workflow_dispatch',
jobResult: 'success',
monorepoRef: 'refs/heads/edge',
variant: 'internal-release',
oeCoreRef: 'refs/heads/main',
firmwareRef: 'refs/heads/main',
consoleUrl: 'https://example.com/console',
systemUrl: 'https://example.com/system.zip',
fullImageUrl: 'https://example.com/full.tar',
versionFileUrl: 'https://example.com/VERSION.json',
releaseNotesUrl: 'https://example.com/release-notes.md',
workflowName: 'Build Flex image on github workflows',
workflowRunUrl: 'https://github.com/Opentrons/oe-core/actions/runs/1',
failedJobs: '',
decision,
})
).toEqual({
tag: 'None',
headline: 'Build artifacts deployed',
status: 'deployed',
's3-url': 'https://example.com/console/',
type: 'branch',
reflike: 'main',
'monorepo-reflike': 'edge',
'firmware-reflike': 'main',
'full-image': 'https://example.com/full.tar',
'system-update': 'https://example.com/system.zip',
'version-file': 'https://example.com/VERSION.json',
'release-notes': 'https://example.com/release-notes.md',
'workflow-name': 'Build Flex image on github workflows',
'workflow-run-url': 'https://github.com/Opentrons/oe-core/actions/runs/1',
'failed-jobs': '',
})
})
})
Loading
Loading