fix(openfeature): isolate malformed flag configuration #3026
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: Pull Request Title | |
| on: | |
| pull_request_target: | |
| types: [opened, edited, reopened, synchronize] | |
| branches: | |
| - "master" | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| conventional-commit: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| env: | |
| # Shared between both steps. Must stay portable across bash ERE and JS | |
| # regex (no lookarounds, named groups, or other JS-only features). | |
| # Revert PRs always get semver-patch regardless of the original change's type. | |
| PR_TITLE_PATTERN: '^(revert(!)?: .+|(feat|fix|docs|style|refactor|perf|test|bench|build|ci|chore)(\(([^)]+)\))?(!)?: .+)' | |
| steps: | |
| - name: Checkout base revision | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.event.pull_request.base.sha }} | |
| persist-credentials: false | |
| - name: Auto-rename GitHub revert title to Conventional Commit | |
| id: rename | |
| if: startsWith(github.event.pull_request.title, 'Revert "') | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const title = context.payload.pull_request.title | |
| const m = title.match(/^Revert "(.+)"$/) | |
| if (!m) return | |
| const newTitle = `revert: ${m[1]}` | |
| core.notice(`Auto-renaming PR title to: ${newTitle}`) | |
| await github.rest.pulls.update({ | |
| ...context.repo, | |
| pull_number: context.payload.pull_request.number, | |
| title: newTitle, | |
| }) | |
| core.setOutput('renamed', 'true') | |
| - name: Validate PR title and release-note context | |
| if: >- | |
| steps.rename.outputs.renamed != 'true' && | |
| (github.event.action != 'edited' || github.event.changes.title != null) | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const pullRequest = context.payload.pull_request | |
| const title = pullRequest.title || '' | |
| const match = title.match(new RegExp(process.env.PR_TITLE_PATTERN)) | |
| if (!match) { | |
| core.setFailed('PR title does not follow Conventional Commits format.') | |
| return | |
| } | |
| core.info(`PR title OK: ${title}`) | |
| const type = match[3] | |
| const changedFiles = await github.paginate(github.rest.pulls.listFiles, { | |
| ...context.repo, | |
| pull_number: pullRequest.number, | |
| per_page: 100, | |
| }) | |
| const files = [] | |
| const { appendChangedPaths, isInternalOnly } = require('./scripts/release/changelog') | |
| appendChangedPaths(files, changedFiles) | |
| if (/^(?:feat|fix|perf|docs)$/.test(type) && isInternalOnly(files)) { | |
| core.setFailed(`PR title type "${type}" is public, but every changed file is internal. ` + | |
| 'Use test, bench, ci, or chore.') | |
| } | |
| - name: Sync labels with PR title | |
| if: steps.rename.outputs.renamed != 'true' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const pattern = new RegExp(process.env.PR_TITLE_PATTERN) | |
| const parse = (title) => { | |
| const m = (title || '').match(pattern) | |
| if (!m) return {} | |
| const isRevert = !m[3] | |
| return { | |
| type: isRevert ? 'revert' : m[3], | |
| scope: isRevert ? undefined : m[5], | |
| breaking: isRevert ? m[2] === '!' : m[6] === '!', | |
| } | |
| } | |
| // Reverts are always semver-patch regardless of the original change's type. | |
| const semverFor = ({ type, breaking }) => { | |
| if (!type) return undefined | |
| if (breaking) return 'semver-major' | |
| if (type === 'feat') return 'semver-minor' | |
| return 'semver-patch' | |
| } | |
| const pr = context.payload.pull_request | |
| const next = parse(pr.title) | |
| // Prefetch all existing repo labels once to avoid per-label API calls. | |
| const repoLabels = new Set() | |
| for await (const page of github.paginate.iterator(github.rest.issues.listLabelsForRepo, { ...context.repo, per_page: 100 })) { | |
| for (const label of page.data) repoLabels.add(label.name) | |
| } | |
| // Returns the set of labels derived from a parsed title. | |
| // Type and scope are only added if they exist in the repo. | |
| const titledLabels = (parsed) => { | |
| const labels = new Set() | |
| if (!parsed.type) return labels | |
| if (repoLabels.has(parsed.type)) labels.add(parsed.type) | |
| if (parsed.scope && repoLabels.has(parsed.scope)) labels.add(parsed.scope) | |
| const semver = semverFor(parsed) | |
| if (semver) labels.add(semver) | |
| return labels | |
| } | |
| const nextLabels = titledLabels(next) | |
| const current = new Set((pr.labels || []).map(l => l.name)) | |
| // When the title changes, remove labels from the old title that no | |
| // longer apply so stale type/scope/semver labels don't linger. | |
| const titleChanged = context.payload.action === 'edited' && | |
| context.payload.changes?.title?.from != null | |
| const toRemove = new Set() | |
| if (titleChanged) { | |
| const prev = parse(context.payload.changes.title.from) | |
| for (const label of titledLabels(prev)) { | |
| if (!nextLabels.has(label)) toRemove.add(label) | |
| } | |
| } | |
| const desired = new Set([...current].filter(l => !toRemove.has(l))) | |
| for (const label of nextLabels) desired.add(label) | |
| const unchanged = current.size === desired.size && [...current].every(l => desired.has(l)) | |
| if (!unchanged) { | |
| await github.rest.issues.setLabels({ | |
| ...context.repo, | |
| issue_number: pr.number, | |
| labels: [...desired], | |
| }) | |
| } |