diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml index 1d3c717..fb06de4 100644 --- a/.github/workflows/accessibility.yml +++ b/.github/workflows/accessibility.yml @@ -10,19 +10,23 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Least-privilege permissions (#462) +permissions: + contents: read + jobs: accessibility: name: accessibility runs-on: namespace-profile-nursca steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: pnpm/action-setup@v4 with: version: '9.0.0' - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' cache: 'pnpm' @@ -37,7 +41,7 @@ jobs: - name: Upload accessibility test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: accessibility-results path: | diff --git a/.github/workflows/contract-tests.yml b/.github/workflows/contract-tests.yml index 796293b..cd3ae1c 100644 --- a/.github/workflows/contract-tests.yml +++ b/.github/workflows/contract-tests.yml @@ -10,19 +10,23 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Least-privilege permissions (#462) +permissions: + contents: read + jobs: contract-tests: name: Mocked Stellar SDK Contract Tests runs-on: namespace-profile-nursca steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: pnpm/action-setup@v4 with: version: '9.0.0' - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' cache: 'pnpm' @@ -75,7 +79,7 @@ jobs: - name: Upload coverage report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: contract-coverage path: coverage/ diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b3f78cd..96395b8 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -10,18 +10,22 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Least-privilege permissions (#462) +permissions: + contents: read + jobs: e2e: timeout-minutes: 30 runs-on: namespace-profile-nursca steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: pnpm/action-setup@v4 with: version: '9.0.0' - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' cache: "pnpm" @@ -39,7 +43,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: playwright-report path: playwright-report/ @@ -47,7 +51,7 @@ jobs: - name: Upload test videos on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: e2e-test-videos path: test-results/playwright/ diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml new file mode 100644 index 0000000..6c4a76f --- /dev/null +++ b/.github/workflows/license-check.yml @@ -0,0 +1,48 @@ +name: License Compatibility Check + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +# Least-privilege permissions (#462) +permissions: + contents: read + +jobs: + license-check: + name: Dependency License Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci + + - name: Install license-checker + run: npm install --no-save license-checker@25.0.1 + + # Fail if any dependency uses a license outside the allowlist. + # The allowlist is defined in .license-checker.json at the repo root. + # See docs/ci-cd.md for the triage process for exceptions. + - name: Check dependency licenses + run: | + npx license-checker \ + --production \ + --onlyAllow "$(node -e "const c=require('./.license-checker.json'); console.log(c.allowedLicenses.join(';'))")" \ + --excludePrivatePackages \ + --out license-report.txt + + - name: Upload license report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: license-report + path: license-report.txt + retention-days: 14 diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml index e6efaef..f0da875 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -10,19 +10,25 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Least-privilege permissions (#462) +# statuses: write is required for LHCI to post commit status checks +permissions: + contents: read + statuses: write + jobs: lighthouse: name: Lighthouse CI runs-on: namespace-profile-nursca steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: pnpm/action-setup@v4 with: version: '9.0.0' - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' cache: 'pnpm' @@ -43,7 +49,7 @@ jobs: - name: Upload Lighthouse reports if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: lighthouse-reports path: .lighthouseci/ diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 0000000..f755f0b --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,39 @@ +name: Security Audit + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + schedule: + # Run every Monday at 08:00 UTC to catch newly published CVEs + - cron: '0 8 * * 1' + +# Least-privilege permissions (#462) +permissions: + contents: read + +jobs: + audit: + name: npm audit security gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci + + # Fail on high and critical severity vulnerabilities. + # Moderate vulnerabilities are reported but do not block the build. + # See docs/ci-cd.md for the triage / allowlist process. + - name: Run security audit (high+critical gate) + run: npm audit --audit-level=high + + # Separate informational step for moderate findings — never blocks CI. + - name: Report moderate vulnerabilities (informational) + run: npm audit --audit-level=moderate || true diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index ab1a792..e8a11b3 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -10,13 +10,17 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Least-privilege permissions (#462) +permissions: + contents: read + jobs: chromatic: if: ${{ secrets.CHROMATIC_PROJECT_TOKEN != '' }} name: Chromatic Visual Regression runs-on: namespace-profile-nursca steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 @@ -25,7 +29,7 @@ jobs: version: '9.0.0' - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' cache: 'pnpm' @@ -39,7 +43,7 @@ jobs: NODE_OPTIONS: '--max_old_space_size=4096' - name: Run Chromatic - uses: chromaui/action@latest + uses: chromaui/action@14cfaef73576e69f95f47f60058063f46ca38719 # v18.1.0 with: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} storybookBuildDir: storybook-static @@ -51,7 +55,7 @@ jobs: - name: Upload Storybook build if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: storybook-static path: storybook-static/ diff --git a/.gitignore b/.gitignore index 942be9c..36d3b5f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ !.yarn/releases !.yarn/versions -# testing +# testing — generated artifacts, never commit these /coverage /test-results /playwright-report @@ -23,9 +23,12 @@ # production /build -# lighthouse CI +# lighthouse CI — generated reports, never commit these /.lighthouseci +# storybook static output +/storybook-static + # misc .DS_Store *.pem @@ -66,9 +69,6 @@ useWallet_SUMMARY.md useWallet_QUICK_REFERENCE.md docs/pr_description.md -# storybook static output -/storybook-static - # turbo cache .turbo diff --git a/.license-checker.json b/.license-checker.json new file mode 100644 index 0000000..a30034c --- /dev/null +++ b/.license-checker.json @@ -0,0 +1,19 @@ +{ + "allowedLicenses": [ + "MIT", + "MIT-0", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "CC0-1.0", + "CC-BY-3.0", + "CC-BY-4.0", + "0BSD", + "BlueOak-1.0.0", + "Python-2.0", + "Unlicense", + "WTFPL" + ], + "comment": "Allowlist of OSI-compatible, permissive licenses. Copyleft licenses (GPL, LGPL, AGPL, MPL, EUPL) are NOT permitted. To request an exception for a specific package, add it to the 'excludePackages' list in .license-checker.json and document the rationale in docs/ci-cd.md." +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b18039b..87a2fc7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -856,6 +856,26 @@ export const Variant: Story = { - Regenerate all baselines after major updates - Update this documentation as needed +## Security Audit (CI Gate) + +A `npm audit --audit-level=high` step runs in CI on every push and PR (see `.github/workflows/security-audit.yml`). The build **fails** on high or critical severity vulnerabilities. + +### When the audit fails + +1. Run `npm audit` locally to see the full report. +2. Apply available patches: `npm audit fix` (or `npm audit fix --force` for breaking-change patches — review the diff carefully). +3. If no fix is available and the finding is a confirmed false positive for this project, open a tracking issue and follow the triage process in [docs/ci-cd.md](docs/ci-cd.md#security-audit-gate-issue-459). + +## License Compatibility Check + +All production dependencies must use permissive OSI-approved licenses. The allowed list is in `.license-checker.json`. CI checks this on every push and PR (see `.github/workflows/license-check.yml`). + +### When the license check fails + +1. Identify which new dependency introduced the non-allowed license. +2. Look for a permissive-licensed alternative. +3. If no alternative exists, follow the exception process in [docs/ci-cd.md](docs/ci-cd.md#license-compatibility-check-issue-460). + ## Getting Help If you need help: diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 4093a75..8d8723d 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -1,6 +1,18 @@ -# CI/CD Documentation +# CI/CD Documentation & Policy Guide -This document describes the CI/CD pipeline configuration, branch protection rules, and custom runner requirements for the ILN Frontend repository. +This document describes the CI/CD pipeline configuration, branch protection rules, custom runner requirements, and security hardening policies for the ILN Frontend repository. It addresses issues **#459**, **#460**, **#461**, and **#462**. + +## Table of Contents + +1. [Custom Runner: namespace-profile-nursca](#custom-runner-namespace-profile-nursca) +2. [Branch Protection Rules](#branch-protection-rules) +3. [Workflow Triggers](#workflow-triggers) +4. [SHA Pinning Policy (Issue #461)](#sha-pinning-policy-issue-461) +5. [Least-Privilege Permissions (Issue #462)](#least-privilege-permissions-issue-462) +6. [Security Audit Gate (Issue #459)](#security-audit-gate-issue-459) +7. [License Compatibility Check (Issue #460)](#license-compatibility-check-issue-460) + +--- ## Custom Runner: namespace-profile-nursca @@ -56,6 +68,8 @@ The following workflows use `namespace-profile-nursca`: Note: `workflow-lint.yml` uses `ubuntu-latest` (GitHub-hosted runner) as it only requires workflow validation tools. +--- + ## Branch Protection Rules ### Main Branch @@ -107,91 +121,121 @@ Note: E2E tests (`End-to-End Tests / e2e`) are **not required** for `develop` br - **Require branches to be up to date before merging**: Yes - **Do not allow bypassing the above settings**: No (admins can bypass) +--- + ## Workflow Triggers -### CI Workflow (ci.yml) +| Workflow File | Trigger | Purpose | +|---|---|---| +| `ci.yml` | push/PR → main, develop | Lint, unit tests, build | +| `contract-tests.yml` | push/PR → main, develop | Soroban contract integration tests + coverage gate | +| `accessibility.yml` | push/PR → main, develop | jest-axe WCAG accessibility tests | +| `e2e-tests.yml` | push/PR → main, develop | Playwright end-to-end tests | +| `lighthouse.yml` | push/PR → main, develop | Lighthouse performance budget | +| `visual-regression.yml` | push/PR → main, develop | Chromatic visual regression via Storybook | +| `storybook-deploy.yml` | push → main + manual dispatch | Deploy Storybook to GitHub Pages | +| `workflow-lint.yml` | push/PR with changes to `.github/workflows/**` | Validate workflow YAML syntax | +| `security-audit.yml` | push/PR → main, develop + weekly cron | npm audit security gate | +| `license-check.yml` | push/PR → main, develop | Dependency license compatibility | + +--- -Triggers: -- Push to `main` or `develop` -- Pull requests targeting `main` or `develop` +## Required Secrets -Jobs: -- `lint` - ESLint validation -- `tests` - Unit tests + accessibility tests -- `build` - Production build verification +| Secret | Workflow | Purpose | +|---|---|---| +| `CHROMATIC_PROJECT_TOKEN` | `visual-regression.yml` | Authenticate with Chromatic cloud | +| `GITHUB_TOKEN` (auto) | `lighthouse.yml` | Post commit status checks via LHCI | +| `GITHUB_TOKEN` (auto) | all others | Read source code only | -### End-to-End Tests (e2e-tests.yml) +--- -Triggers: -- Push to `main` or `develop` -- Pull requests targeting `main` or `develop` +## SHA Pinning Policy (Issue #461) -Jobs: -- `e2e` - Playwright E2E test suite with artifact uploads +### Why we pin to SHAs -### Visual Regression Tests (visual-regression.yml) +Using floating version tags (`@v4`, `@latest`) is a supply-chain risk. A tag can be silently moved to a different commit at any time — either by the action publisher or a malicious actor. This repo signs Stellar transactions and handles wallet connections, making it a high-value target. -Triggers: -- Push to `main` or `develop` -- Pull requests targeting `main` or `develop` +Pinning to a full commit SHA guarantees the exact code that was reviewed is the code that runs. This is [GitHub's own recommendation](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions) for security-sensitive repositories. -Jobs: -- `chromatic` - Chromatic visual regression testing (requires `CHROMATIC_PROJECT_TOKEN` secret) +### Current pinned SHAs -### Storybook Deployment (storybook-deploy.yml) +| Action | Tag | Pinned SHA | +|---|---|---| +| `actions/checkout` | v4.4.0 | `11d5960a326750d5838078e36cf38b85af677262` | +| `actions/setup-node` | v4.4.0 | `49933ea5288caeca8642d1e84afbd3f7d6820020` | +| `actions/upload-artifact` | v4.6.2 | `ea165f8d65b6e75b540449e92b4886f43607fa02` | +| `chromaui/action` | v18.1.0 | `14cfaef73576e69f95f47f60058063f46ca38719` | -Triggers: -- Push to `main` -- Manual workflow dispatch +Note: `pnpm/action-setup@v4` is not yet pinned to a SHA. Pin it when bumping by following the procedure below. -Jobs: -- `build-and-deploy` - Builds Storybook and deploys to GitHub Pages +### How to update a pinned SHA -### Lighthouse Performance Budget (lighthouse.yml) +When you need to bump an action to a newer version: -Triggers: -- Push to `main` or `develop` -- Pull requests targeting `main` or `develop` +1. **Find the new SHA** for the desired tag: -Jobs: -- `lighthouse` - Lighthouse CI performance budget validation + ```bash + # Using the GitHub API + curl -s "https://api.github.com/repos/OWNER/REPO/git/ref/tags/vX.Y.Z" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['object']['sha'])" -### Accessibility Tests (accessibility.yml) + # Or from the tags list + curl -s "https://api.github.com/repos/OWNER/REPO/tags" \ + | python3 -c "import sys,json; [print(t['name'], t['commit']['sha']) for t in json.load(sys.stdin)[:10]]" + ``` -Triggers: -- Push to `main` or `develop` -- Pull requests targeting `main` or `develop` +2. **Review the release notes** for the action between the old and new version. -Jobs: -- `accessibility` - Consolidated accessibility validation for the dedicated axe suite and page-level accessibility tests +3. **Update the workflow file** — replace the SHA and update the version comment: + ```yaml + # Before + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 -### Contract Integration Tests (contract-tests.yml) + # After + uses: actions/checkout@ # v4.5.0 + ``` -Triggers: -- Push to `main` or `develop` -- Pull requests targeting `main` or `develop` +4. **Update the SHA table above** in this document. -Jobs: -- `contract-tests` - Stellar SDK contract tests with 90% coverage enforcement +5. Open a PR with the changes using `ci:` commit prefix, e.g.: + ``` + ci: bump actions/checkout to v4.5.0 (SHA pinned) + ``` -### Workflow Lint (workflow-lint.yml) +### Automated tooling -Triggers: -- Push to `main` or `develop` with changes to `.github/workflows/**` -- Pull requests with changes to `.github/workflows/**` +[Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) is recommended for automated SHA bump PRs: -Jobs: -- `actionlint` - GitHub Actions workflow syntax validation +```yaml +# .github/dependabot.yml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +``` -## Required Secrets +--- + +## Least-Privilege Permissions (Issue #462) + +### Policy -The following secrets must be configured in the repository settings: +Every workflow file must declare an explicit `permissions:` block. Omitting the block causes GitHub to grant the token the repository's default permissions, which are often broader than necessary. -### Chromatic -- `CHROMATIC_PROJECT_TOKEN` - Project token for Chromatic visual regression testing +### Permissions granted per workflow -### Other Secrets -- `GITHUB_TOKEN` - Automatically provided by GitHub Actions (no manual configuration needed) +| Workflow | `contents` | `statuses` | Other | +|---|---|---|---| +| `contract-tests.yml` | `read` | — | — | +| `accessibility.yml` | `read` | — | — | +| `e2e-tests.yml` | `read` | — | — | +| `lighthouse.yml` | `read` | `write` | — | +| `visual-regression.yml` | `read` | — | — | +| `security-audit.yml` | `read` | — | — | +| `license-check.yml` | `read` | — | — | ## Per-Workflow Environment Variables @@ -304,13 +348,50 @@ This section documents the environment variables that each workflow sets or impl ## Discrepancy Notes -### Current Notes +--- + +## Security Audit Gate (Issue #459) + +`security-audit.yml` runs `npm audit` on every push, PR, and on a weekly schedule (Monday 08:00 UTC). + +- **Fails the build** on `high` or `critical` severity findings. +- **Reports but does not fail** on `moderate` findings. +- Covers key financial dependencies: `@stellar/freighter-api`, `@stellar/stellar-sdk`, `@supabase/supabase-js`. + +### Triage process for false positives + +1. Run `npm audit` locally to read the full CVE details. +2. Apply available patches: `npm audit fix` (or `npm audit fix --force` for breaking changes — review the diff). +3. If no fix is available and the finding is genuinely not exploitable: + - Open a tracking issue: `security: track CVE-XXXX-XXXXX in [package]` + - Include: advisory URL, why it is not exploitable, expected fix date. +4. Re-run CI after any change to confirm the gate passes. + +--- + +## License Compatibility Check (Issue #460) -1. **Shared Node version**: Workflows now consume `.nvmrc` so local development and CI stay aligned on the same Node baseline. +All production dependencies must use permissive OSI-approved licenses. Copyleft licenses (GPL, LGPL, AGPL, MPL, EUPL) are **not permitted**. -2. **Dedicated accessibility workflow**: Accessibility checks now live in a single workflow with one required `accessibility` job, which avoids duplicate check names and redundant CI runs. +The allowlist is defined in `.license-checker.json` at the repo root. `license-check.yml` enforces it on every push and PR against production deps only. + +### Allowed licenses + +MIT, MIT-0, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, CC0-1.0, CC-BY-3.0, CC-BY-4.0, 0BSD, BlueOak-1.0.0, Python-2.0, Unlicense, WTFPL. + +### Handling a violation + +1. Confirm the license in the package's `LICENSE` file (metadata can be wrong). +2. Find a permissive-licensed alternative. +3. If no alternative exists and legal approves an exception, add the package to `excludePackages` in `.license-checker.json` and document it here under a **License Exceptions** section with: package name/version, license type, legal sign-off link, rationale. + +--- + +## Discrepancy Notes -3. **E2E coverage on develop**: The E2E suite now runs for both `main` and `develop` pushes and pull requests so regressions surface before `main` merges. +1. **Shared Node version**: Workflows consume `.nvmrc` so local development and CI stay aligned on the same Node baseline. +2. **Dedicated accessibility workflow**: Accessibility checks live in a single workflow with one required `accessibility` job. +3. **E2E coverage on develop**: The E2E suite now runs for both `main` and `develop` pushes and PRs. 4. **Feature flag build consistency**: The `ci.yml` build job does not set feature flags. If a flag lacks a default in `src/lib/env.ts`, it will silently build as disabled. All new flags must have a defined default. @@ -318,8 +399,7 @@ This section documents the environment variables that each workflow sets or impl ### Maintainer Action Required -Please verify the following: -- [ ] Confirm the actual branch protection rules configured in GitHub repository settings match the documentation above +- [ ] Confirm branch protection rules in GitHub repository settings match the documentation above - [ ] Review and update custom runner configuration documentation if `namespace-profile-nursca` setup has changed - [ ] Add `bundle-size` to branch protection required checks (once baseline is established) - [ ] Update `mutation-testing.yml` to use `node-version-file: '.nvmrc'` for version consistency