diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 9fe54a78..f282ebda 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -3,14 +3,18 @@ on: push: tags: - '*' + +permissions: + contents: write + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - name: Create a Release - uses: elgohr/Github-Release-Action@v5 - env: - GITHUB_TOKEN: "${{ secrets.RELEASE_TOKEN }}" - with: - title: ${{ github.ref }} + - uses: actions/checkout@v6 + - name: Create a Release + uses: elgohr/Github-Release-Action@v5 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + with: + title: ${{ github.ref }} diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 19d95359..fa4abf20 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ master ] +permissions: + contents: read + jobs: build: runs-on: ${{ matrix.os }} @@ -22,12 +25,26 @@ jobs: - name: Install dependencies run: dart pub get - - name: Dart Analyzer - run: dart analyze + + - name: Dart Analyzer (Strict mode) + run: dart analyze --fatal-infos + - name: Check Dart Format if: ${{ matrix.sdk == 'stable' }} run: dart format --set-exit-if-changed -onone . - - name: Unit tests + + - name: Verify Spec Sync Drift + if: ${{ matrix.sdk == 'stable' }} + run: dart run tool/spec_sync.dart --check --offline + + - name: Unit and Contract Tests run: dart test + + - name: Coverage Ratchet Check + if: ${{ matrix.sdk == 'stable' }} + run: | + dart test --coverage=coverage + dart run tool/coverage_check.dart coverage/lcov.info --min=30.0 + - name: Check if Publishable run: dart pub publish --dry-run diff --git a/.github/workflows/publish_demos.yml b/.github/workflows/publish_demos.yml index 7e13bac4..2605b329 100644 --- a/.github/workflows/publish_demos.yml +++ b/.github/workflows/publish_demos.yml @@ -3,6 +3,10 @@ on: push: branches: - master + +permissions: + contents: write + jobs: build-and-deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/spec_sync.yml b/.github/workflows/spec_sync.yml new file mode 100644 index 00000000..49cc8d70 --- /dev/null +++ b/.github/workflows/spec_sync.yml @@ -0,0 +1,98 @@ +name: Weekly GitHub OpenAPI Spec Sync + +on: + schedule: + # Run every Monday at 04:00 UTC + - cron: '0 4 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + spec-sync: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Dart SDK + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Install dependencies + run: dart pub get + + - name: Check for upstream OpenAPI specification updates + id: check_upstream + run: | + mkdir -p .dart_tool + dart run tool/spec_sync.dart --offline --report-json > .dart_tool/spec_report_before.json + echo "Checked current spec baseline." + + - name: Regenerate OpenAPI catalog and docs + id: run_sync + run: | + dart run tool/spec_sync.dart --update + git status --porcelain > .dart_tool/git_status.txt + if [ -s .dart_tool/git_status.txt ]; then + echo "changes_detected=true" >> "$GITHUB_OUTPUT" + else + echo "changes_detected=false" >> "$GITHUB_OUTPUT" + fi + + - name: Validate generated code and contracts + if: steps.run_sync.outputs.changes_detected == 'true' + run: | + dart format --output=none --set-exit-if-changed . + dart analyze --fatal-infos + dart test + dart pub publish --dry-run + + - name: Create or update spec-sync pull request + if: steps.run_sync.outputs.changes_detected == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH_NAME="automation/github-openapi-spec-sync" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git checkout -B "$BRANCH_NAME" + git add lib/src/common/generated/rest_contracts.g.dart docs/api-coverage.md tool/spec/ + git commit -m "chore(spec): synchronize GitHub OpenAPI specification contracts" + git push --force origin "$BRANCH_NAME" + + PR_TITLE="chore(spec): synchronize GitHub OpenAPI specification contracts" + PR_BODY="## GitHub OpenAPI Specification Sync + + This automated pull request synchronizes the GitHub OpenAPI description contracts and coverage reports. + + ### Summary of Changes + - Regenerated \`lib/src/common/generated/rest_contracts.g.dart\` + - Updated API coverage documentation in \`docs/api-coverage.md\` + + ### Validation + - \`dart format\` verified + - \`dart analyze --fatal-infos\` passed + - \`dart test\` passed + - \`dart pub publish --dry-run\` verified + + **Notice**: Review only. Never auto-merged." + + gh pr create \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --base master \ + --head "$BRANCH_NAME" \ + --label "automation,spec-sync" || \ + gh pr edit "$BRANCH_NAME" \ + --title "$PR_TITLE" \ + --body "$PR_BODY" diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 8b71d9ae..cd49b23a 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -3,6 +3,9 @@ on: issues: types: [opened] +permissions: + issues: write + jobs: assignRob: name: Assign Rob diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b574e5a8..b6ea9e48 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,20 @@ To regenerate the JSON logic for the models, run: dart run build_runner build -d ``` +## OpenAPI Specification & Contract Sync + +This repository uses an OpenAPI-driven contract verification system pinned to the official GitHub REST API specification. + +- To check that code and documentation match the pinned OpenAPI specification: + ```sh + dart run tool/spec_sync.dart --check --offline + ``` +- To update or regenerate the REST contracts catalog and coverage report: + ```sh + dart run tool/spec_sync.dart --update + ``` +- Automated weekly pull requests (`spec_sync.yml`) monitor upstream API specification releases. Automated PRs are review-only and never auto-merged. + ## Tests `dart test` will only run the unit tests. diff --git a/README.md b/README.md index 08dc8e09..12b8ef09 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,35 @@ We are looking for contributors. If you're interested or have questions, head ov - [Library Demos](https://spinlocklabs.github.io/github.dart/) (based on the [sample code](https://github.com/SpinlockLabs/github.dart/tree/master/example)) - [Pub Package](https://pub.dev/packages/github) +- [API Coverage Report](docs/api-coverage.md) - [Wiki](https://github.com/SpinlockLabs/github.dart/wiki) - [Latest API reference](https://pub.dev/documentation/github/latest/) +## OpenAPI Conformance and Spec Synchronization + +This library tracks the official [GitHub REST API v3 description](https://github.com/github/rest-api-description). +- The OpenAPI specification baseline is pinned in `tool/spec/github-openapi.lock.json`. +- Operation bindings and conformance are tracked in `tool/spec/bindings.yaml`. +- Immutable REST contract metadata is generated into `lib/src/common/generated/rest_contracts.g.dart`. +- Conformance and coverage metrics are documented in `docs/api-coverage.md`. + +To check or update the OpenAPI contracts locally: +```bash +# Verify no drift between specification and codebase +dart run tool/spec_sync.dart --check --offline + +# Regenerate contracts and documentation +dart run tool/spec_sync.dart --update +``` + +A weekly GitHub Actions workflow runs the spec sync check and opens review-only PRs when upstream API descriptions evolve. + +## Security and Best Practices + +- **Credential Boundaries**: Authenticated requests are constrained to the configured API origin (`api.github.com` or custom enterprise endpoint). Cross-origin redirects and pagination links automatically strip credentials. Insecure HTTP authentication is disallowed by default. +- **Webhooks**: Always configure a webhook secret. Webhooks are validated using constant-time HMAC-SHA256 signature verification, enforce configurable payload limits, and support replay attack detection. +- **Browser Authentication**: Token extraction from URL query parameters is disabled by default for security. Tokens should be passed via `Authentication.withToken()` or session storage. + ## Examples See the examples in the example directory to learn how to use some of the features!