Change release process to use changie.dev#1117
Conversation
| name: Changelog fragment present and valid | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Skip on merge_group | ||
| if: github.event_name == 'merge_group' | ||
| run: echo "Skipping changelog check on merge_group" | ||
|
|
||
| - uses: actions/checkout@v4 | ||
| if: github.event_name != 'merge_group' | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Fetch base branch | ||
| if: github.event_name != 'merge_group' | ||
| run: git fetch origin ${{ github.event.pull_request.base.ref }} | ||
|
|
||
| - name: Check for no-changelog label | ||
| if: github.event_name != 'merge_group' | ||
| id: check-label | ||
| run: | | ||
| labels='${{ toJSON(github.event.pull_request.labels.*.name) }}' | ||
| if python3 -c " | ||
| import sys, json | ||
| labels = json.loads('$labels') | ||
| sys.exit(0 if 'no-changelog' in labels else 1) | ||
| " 2>/dev/null; then | ||
| echo "skip=true" >> "$GITHUB_OUTPUT" | ||
| echo "PR has 'no-changelog' label — skipping fragment check." | ||
| else | ||
| echo "skip=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Find changelog fragments added in this PR | ||
| if: github.event_name != 'merge_group' && steps.check-label.outputs.skip != 'true' | ||
| id: find-fragments | ||
| run: | | ||
| BASE="origin/${{ github.event.pull_request.base.ref }}" | ||
| FRAGMENTS=$(git diff --name-only --diff-filter=A "$BASE...HEAD" \ | ||
| | grep '^\.changes/unreleased/.*\.yaml$' || true) | ||
|
|
||
| if [ -z "$FRAGMENTS" ]; then | ||
| echo "found=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "found=true" >> "$GITHUB_OUTPUT" | ||
| # Use random EOF delimiter to safely handle arbitrary fragment paths | ||
| EOF_DELIM=$(openssl rand -hex 8) | ||
| { | ||
| echo "fragments<<${EOF_DELIM}" | ||
| echo "$FRAGMENTS" | ||
| echo "${EOF_DELIM}" | ||
| } >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Fail if no fragment found | ||
| if: >- | ||
| github.event_name != 'merge_group' | ||
| && steps.check-label.outputs.skip != 'true' | ||
| && steps.find-fragments.outputs.found == 'false' | ||
| run: | | ||
| echo "::error::No changelog fragment found in .changes/unreleased/" | ||
| echo "" | ||
| echo "Every PR that changes user-visible behaviour must include a changie fragment." | ||
| echo "To add one, run:" | ||
| echo "" | ||
| echo " changie new --project <package>" | ||
| echo "" | ||
| echo "and commit the generated file." | ||
| echo "Add the 'no-changelog' label to skip this check for maintenance / test-only PRs." | ||
| exit 1 | ||
|
|
||
| - name: Validate fragment files | ||
| if: >- | ||
| github.event_name != 'merge_group' | ||
| && steps.check-label.outputs.skip != 'true' | ||
| && steps.find-fragments.outputs.found == 'true' | ||
| env: | ||
| FRAGMENTS: ${{ steps.find-fragments.outputs.fragments }} | ||
| run: | | ||
| python3 << 'PYEOF' | ||
| import sys | ||
| import os | ||
| import yaml | ||
|
|
||
| VALID_KINDS = {'breaking', 'feature', 'compatible', 'bugfix', 'optimisation'} | ||
|
|
||
| fragments = [f for f in os.environ['FRAGMENTS'].strip().split('\n') if f] | ||
| all_ok = True | ||
|
|
||
| for path in fragments: | ||
| try: | ||
| with open(path) as fh: | ||
| fragment = yaml.safe_load(fh) | ||
| except Exception as exc: | ||
| print(f"::error file={path}::Failed to parse YAML: {exc}") | ||
| all_ok = False | ||
| continue | ||
|
|
||
| if not isinstance(fragment, dict): | ||
| print(f"::error file={path}::Fragment must be a YAML mapping, got {type(fragment).__name__}") | ||
| all_ok = False | ||
| continue | ||
|
|
||
| errors = [] | ||
|
|
||
| kind = fragment.get('kind') | ||
| if not kind: | ||
| errors.append("missing required field 'kind'") | ||
| elif kind not in VALID_KINDS: | ||
| errors.append( | ||
| f"invalid kind '{kind}'; must be one of: {', '.join(sorted(VALID_KINDS))}" | ||
| ) | ||
|
|
||
| if not fragment.get('body', '').strip(): | ||
| errors.append("missing or empty required field 'body'") | ||
|
|
||
| custom = fragment.get('custom') or {} | ||
| if 'PR' not in custom: | ||
| errors.append("missing required field 'custom.PR' (PR number)") | ||
|
|
||
| if errors: | ||
| for err in errors: | ||
| print(f"::error file={path}::{err}") | ||
| all_ok = False | ||
| else: | ||
| print(f"Fragment OK: {path} (kind={kind}, PR={custom.get('PR')})") | ||
|
|
||
| sys.exit(0 if all_ok else 1) | ||
| PYEOF |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 11 days ago
In general, the fix is to explicitly define the GITHUB_TOKEN permissions for this workflow or job so that they are as restrictive as possible while still allowing the job to function. Since this job only checks changelog fragments and does not modify repository contents or PRs, it only needs read access to repository contents. GitHub recommends starting from contents: read as a minimal safe baseline.
The best targeted fix here is to add a permissions block for the check-fragment job, directly under the job’s name or runs-on keys. That keeps the scope narrow and avoids affecting other workflows. We will set contents: read, which is sufficient for actions/checkout and any local git operations. No other permission types (like pull-requests or issues) are required because the job only uses the event payload already provided by GitHub, not additional API calls.
Concretely: in .github/workflows/check-changelog-fragment.yml, within the jobs: check-fragment: section starting at line 9, insert:
permissions:
contents: readindented to align with name: and runs-on:. No imports, methods, or additional definitions are needed because this is a pure YAML configuration change.
| @@ -9,6 +9,8 @@ | ||
| check-fragment: | ||
| name: Changelog fragment present and valid | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - name: Skip on merge_group | ||
| if: github.event_name == 'merge_group' |
| name: check-changelog (superseded — see check-changelog-fragment.yml) | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| if: ${{ github.event_name != 'merge_group' }} | ||
| with: | ||
| node-version: 22 | ||
|
|
||
| - run: npm install js-yaml@4.1.0 | ||
| if: ${{ github.event_name != 'merge_group' }} | ||
|
|
||
| - name: Fail if PR changelog is not correct | ||
| if: ${{ github.event_name != 'merge_group' }} | ||
| uses: actions/github-script@v8 | ||
| id: check-changelog | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const yaml = require('js-yaml'); | ||
| const fs = require('fs'); | ||
| const execSync = require('child_process').execSync; | ||
|
|
||
| const prDescription = await github.rest.pulls.get({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: context.issue.number | ||
| }); | ||
|
|
||
| const changelogRegex = /# Changelog[\s\S]*?```yaml([\s\S]*?)```/; | ||
| const changelogMatch = prDescription.data.body.match(changelogRegex); | ||
| const yamlContent = changelogMatch ? changelogMatch[1].trim() : ''; | ||
| yamlContent || console.error('Failed to find changelog YAML section in the "Changelog" paragraph'); | ||
|
|
||
| try { | ||
| changelog = yaml.load(yamlContent)[0]; | ||
| } catch (e) { | ||
| console.error('Failed to parse YAML changelog as array:', yamlContent); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| try { | ||
| config = yaml.load(fs.readFileSync('.cardano-dev.yaml', 'utf8')); | ||
| } catch (e) { | ||
| console.error('Failed to load .cardano-dev.yaml config:', e); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| let isCompatibilityValid = false; | ||
| if (!changelog.compatibility) { | ||
| isCompatibilityValid = true; | ||
| } | ||
| if (!isCompatibilityValid) { | ||
| console.error('Changelog field "compatibility" is deprecated and no longer used. Please remove it.'); | ||
| } | ||
|
|
||
| let isTypeValid = false; | ||
| const validTypeValues = Object.keys(config.changelog.options.type); | ||
| if (Array.isArray(changelog.type) && !!changelog.type) { | ||
| isTypeValid = changelog.type.every(value => validTypeValues.includes(value)); | ||
| } else { | ||
| isTypeValid = validTypeValues.includes(changelog.type); | ||
| } | ||
| if (!isTypeValid) { | ||
| console.error(`PR changelog has invalid type: ${changelog.type}\nExpected one, or more of: ${validTypeValues}`) | ||
| } | ||
|
|
||
| let isProjectsValid = false; | ||
| // .filter(Boolean) is a trick that removes empty values from the array (see https://michaeluloth.com/javascript-filter-boolean/) | ||
| const validProjectsValues = execSync("ls */CHANGELOG* | cut -d/ -f1").toString().split('\n').filter(Boolean) | ||
| if (Array.isArray(changelog.projects) && !!changelog.projects) { | ||
| isProjectsValid = changelog.projects.every(value => validProjectsValues.includes(value)); | ||
| } else { | ||
| isProjectsValid = validProjectsValue.includes(changelog.projects); | ||
| } | ||
| if (!isProjectsValid) { | ||
| console.error(`PR changelog has invalid project: ${changelog.projects}\nExpected one, or more of: ${validProjectsValues}`) | ||
| } | ||
|
|
||
| let isDescriptionValid = true; | ||
| if (changelog.description.trim() === '<insert-changelog-description-here>') { | ||
| console.error('PR changelog description has not been updated!') | ||
| isDescriptionValid = false; | ||
| } else if (!changelog.description.trim()) { | ||
| console.error('PR changelog description field is missing!') | ||
| isDescriptionValid = false; | ||
| } | ||
|
|
||
| if (!isCompatibilityValid || !isTypeValid || !isProjectsValid || !isDescriptionValid) { | ||
| console.error('Failed PR changelog checks!'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| - name: Pass (no-op) | ||
| run: | | ||
| echo "This check is a no-op. Changelog validation is now performed by" | ||
| echo "the 'check-changelog-fragment' workflow (check-changelog-fragment.yml)." |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 11 days ago
To fix the problem, explicitly restrict the GITHUB_TOKEN permissions for this workflow to the minimum required. Since this job is a pure no-op that only runs echo commands and does not interact with the repository or GitHub APIs, it can safely run with contents: read (or even contents: none if supported). Setting the permissions at the workflow root applies them to all jobs in this file and best matches the CodeQL recommendation.
The single best fix without changing functionality is to add a permissions block at the top level of .github/workflows/check-pr-changelog.yml, alongside name and on. Insert this block after the name: declaration (around line 11), for example:
permissions:
contents: readNo imports or additional methods are needed, since this is a YAML workflow file, not application code.
| @@ -8,6 +8,9 @@ | ||
|
|
||
| name: Check if PR changelog was filled correctly (legacy — no-op) | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| merge_group: | ||
| pull_request: |
Changelog
Context
Additional context for the PR goes here. If the PR fixes a particular issue please provide a link to the issue.
How to trust this PR
Highlight important bits of the PR that will make the review faster. If there are commands the reviewer can run to observe the new behavior, describe them.
Checklist