From 4031d544f124fe8e5ab81c2eb0da6d5a9dbbee46 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Wed, 27 May 2026 10:15:26 -0300 Subject: [PATCH 1/3] docs: add design spec for GitHub App release authentication --- ...26-05-27-github-app-release-auth-design.md | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-27-github-app-release-auth-design.md diff --git a/docs/superpowers/specs/2026-05-27-github-app-release-auth-design.md b/docs/superpowers/specs/2026-05-27-github-app-release-auth-design.md new file mode 100644 index 0000000..57f9870 --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-github-app-release-auth-design.md @@ -0,0 +1,88 @@ +# GitHub App Authentication for Release Workflows + +**Date:** 2026-05-27 +**Status:** Approved + +## Problem + +Branch protection rules on `main` require PRs and status checks. The `GITHUB_TOKEN` cannot bypass these protections, forcing a manual bypass whenever an automated release needs to push to `main`. This applies to both release reusable workflows: `release.yml` (release-please) and `changelog-release.yml` (custom changelog). + +## Solution + +Add optional GitHub App authentication to both reusable workflows. When the App credentials are provided, a short-lived token is generated and used in place of `GITHUB_TOKEN`. When not provided, the workflows fall back to `GITHUB_TOKEN` as today — maintaining full backwards compatibility. + +## Parameters + +Both workflows receive two new optional secrets: + +| Secret name (in workflow) | Maps to (in caller) | Description | +|---|---|---| +| `app_id` | `APP_RELEASE_ID` | GitHub App numeric ID (not sensitive but passed as secret for consistency) | +| `app_private_key` | `APP_RELEASE_PRIVATE_KEY` | GitHub App private key (PEM format) | + +The App must be installed on the target repos and have: +- **Contents**: Read & write +- **Pull requests**: Read & write + +## Token Generation + +At the start of each job, a conditional step generates the token: + +```yaml +- name: Generate GitHub App token + id: app-token + if: ${{ secrets.app_id != '' }} + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.app_id }} + private-key: ${{ secrets.app_private_key }} +``` + +The effective token is resolved as: +``` +${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} +``` + +If `app_id` is empty, the step is skipped, the output is empty, and the expression falls back to `GITHUB_TOKEN`. + +## Changes per Workflow + +### `release.yml` (tofu-release) + +Two jobs need independent token generation (jobs don't share step outputs). + +**`release` job:** +- Add token generation step +- Pass token to `release-please-action` via `token:` input + +**`update-readme-versions` job:** +- Add token generation step +- Pass token to `actions/checkout@v6` via `token:` input +- `git push` inherits the token from checkout automatically + +### `changelog-release.yml` + +Single job, three token injection points: + +| Step | Change | +|---|---| +| `actions/checkout@v6` | `token:` → effective token | +| `git push` | inherits from checkout | +| `gh release create` | `GH_TOKEN:` env → effective token | + +## Caller Usage Example + +```yaml +jobs: + release: + uses: nullplatform/actions-nullplatform/.github/workflows/changelog-release.yml@main + with: + project-type: npm + secrets: + app_id: ${{ secrets.APP_RELEASE_ID }} + app_private_key: ${{ secrets.APP_RELEASE_PRIVATE_KEY }} +``` + +## Backwards Compatibility + +Callers that do not pass `app_id` / `app_private_key` continue to work with `GITHUB_TOKEN` unchanged. From 47ae6eadcaaf644c7f80959a0b510168a4b5bc88 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Wed, 27 May 2026 10:16:58 -0300 Subject: [PATCH 2/3] chore: remove spec files --- .../2026-05-20-trivy-tofu-scan-design.md | 91 ------------------- ...26-05-27-github-app-release-auth-design.md | 88 ------------------ 2 files changed, 179 deletions(-) delete mode 100644 docs/superpowers/specs/2026-05-20-trivy-tofu-scan-design.md delete mode 100644 docs/superpowers/specs/2026-05-27-github-app-release-auth-design.md diff --git a/docs/superpowers/specs/2026-05-20-trivy-tofu-scan-design.md b/docs/superpowers/specs/2026-05-20-trivy-tofu-scan-design.md deleted file mode 100644 index 21b78a8..0000000 --- a/docs/superpowers/specs/2026-05-20-trivy-tofu-scan-design.md +++ /dev/null @@ -1,91 +0,0 @@ -# Design: trivy-tofu-scan reusable workflow - -**Date:** 2026-05-20 -**Author:** David Fernandez -**Status:** Approved - -## Summary - -Replace `tfsec.yml` with a new reusable workflow `trivy-tofu-scan.yml` that uses Trivy to scan OpenTofu/Terraform IaC code for misconfigurations. The workflow generates a full report persisted in four ways: SARIF upload to the GitHub Security tab, JSON artifact, Job Summary, and PR comment. - -## Context - -The repo already has `tfsec.yml` for IaC security scanning. Trivy covers the same surface (OpenTofu/Terraform misconfigurations) via `--scanners misconfig` and provides richer output options, active maintenance, and a unified tool already used in `docker-security-scan.yml` and `ecr-security-scan.yml`. - -## Architecture - -A single file `.github/workflows/trivy-tofu-scan.yml` with `on: workflow_call`. Follows the exact pattern of `tfsec.yml`, `docker-security-scan.yml`, and `ecr-security-scan.yml`. - -### Permissions - -```yaml -permissions: - contents: read - security-events: write # SARIF upload to GitHub Security tab - pull-requests: write # PR comment -``` - -### Inputs - -| Input | Type | Default | Description | -|---|---|---|---| -| `upload_sarif` | boolean | `true` | Upload SARIF to GitHub Security tab | -| `post_comment` | boolean | `true` | Post comment on PR if findings found | - -Severity is hardcoded to `CRITICAL,HIGH` — not configurable. - -## Job: `trivy-iac` - -Runner: `ubuntu-24.04`. Env: `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true`. - -### Steps - -1. **Checkout** — `actions/checkout@v6` - -2. **Find .tf files** — `find . -name "*.tf" -not -path "./.terraform/*"`. Sets `has_tf_files` output. Early exits (skip remaining steps) if no `.tf` files found. - -3. **Run Trivy IaC scan** — Installs Trivy CLI (pinned version), runs: - ``` - trivy config . --scanners misconfig --severity CRITICAL,HIGH \ - --format json --output trivy-results.json \ - --exit-code 1 - ``` - Uses `continue-on-error: true` to allow subsequent reporting steps to run. Captures exit code in step output. - -4. **Run Trivy SARIF export** — Re-runs Trivy with `--format sarif --output results.sarif --soft-fail`. Only runs if `has_tf_files == 'true'` and `inputs.upload_sarif`. - -5. **Upload SARIF** — `github/codeql-action/upload-sarif@v4` with `category: trivy-iac`. Runs if `upload_sarif` input is true and `results.sarif` exists. - -6. **Generate Job Summary** — Bash script parses `trivy-results.json` with `jq`, builds a markdown table of findings (ID, severity, resource, message) and writes to `$GITHUB_STEP_SUMMARY`. Always runs if `has_tf_files == 'true'`. - -7. **Upload artifact** — `actions/upload-artifact@v4` uploads `trivy-results.json` as `trivy-iac-scan-results`. Always runs if `has_tf_files == 'true'` (even on clean scans — artifact confirms the scan ran). - -8. **Post PR comment** — `actions/github-script@v9` posts a comment with finding count and link to the run. Runs if `post_comment` is true, `github.event_name == 'pull_request'`, and findings were found. - -9. **Fail if findings** — `run: exit 1` if Trivy step exit code was non-zero. - -## Error handling - -- No `.tf` files: steps 3–9 are skipped via `if: steps.find.outputs.has_tf_files == 'true'`. Workflow exits green. -- SARIF upload failure: `continue-on-error: true` so it doesn't block the fail step. -- Trivy install failure: the job fails immediately (no `continue-on-error`). - -## Migration from tfsec - -Callers replace: -```yaml -uses: nullplatform/actions-nullplatform/.github/workflows/tfsec.yml@main -``` -with: -```yaml -uses: nullplatform/actions-nullplatform/.github/workflows/trivy-tofu-scan.yml@main -``` - -The `minimum_severity` input from `tfsec.yml` has no equivalent — severity is fixed to `CRITICAL,HIGH`. - -## Files - -| Action | File | -|---|---| -| Create | `.github/workflows/trivy-tofu-scan.yml` | -| Delete (or deprecate) | `.github/workflows/tfsec.yml` | diff --git a/docs/superpowers/specs/2026-05-27-github-app-release-auth-design.md b/docs/superpowers/specs/2026-05-27-github-app-release-auth-design.md deleted file mode 100644 index 57f9870..0000000 --- a/docs/superpowers/specs/2026-05-27-github-app-release-auth-design.md +++ /dev/null @@ -1,88 +0,0 @@ -# GitHub App Authentication for Release Workflows - -**Date:** 2026-05-27 -**Status:** Approved - -## Problem - -Branch protection rules on `main` require PRs and status checks. The `GITHUB_TOKEN` cannot bypass these protections, forcing a manual bypass whenever an automated release needs to push to `main`. This applies to both release reusable workflows: `release.yml` (release-please) and `changelog-release.yml` (custom changelog). - -## Solution - -Add optional GitHub App authentication to both reusable workflows. When the App credentials are provided, a short-lived token is generated and used in place of `GITHUB_TOKEN`. When not provided, the workflows fall back to `GITHUB_TOKEN` as today — maintaining full backwards compatibility. - -## Parameters - -Both workflows receive two new optional secrets: - -| Secret name (in workflow) | Maps to (in caller) | Description | -|---|---|---| -| `app_id` | `APP_RELEASE_ID` | GitHub App numeric ID (not sensitive but passed as secret for consistency) | -| `app_private_key` | `APP_RELEASE_PRIVATE_KEY` | GitHub App private key (PEM format) | - -The App must be installed on the target repos and have: -- **Contents**: Read & write -- **Pull requests**: Read & write - -## Token Generation - -At the start of each job, a conditional step generates the token: - -```yaml -- name: Generate GitHub App token - id: app-token - if: ${{ secrets.app_id != '' }} - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ secrets.app_id }} - private-key: ${{ secrets.app_private_key }} -``` - -The effective token is resolved as: -``` -${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} -``` - -If `app_id` is empty, the step is skipped, the output is empty, and the expression falls back to `GITHUB_TOKEN`. - -## Changes per Workflow - -### `release.yml` (tofu-release) - -Two jobs need independent token generation (jobs don't share step outputs). - -**`release` job:** -- Add token generation step -- Pass token to `release-please-action` via `token:` input - -**`update-readme-versions` job:** -- Add token generation step -- Pass token to `actions/checkout@v6` via `token:` input -- `git push` inherits the token from checkout automatically - -### `changelog-release.yml` - -Single job, three token injection points: - -| Step | Change | -|---|---| -| `actions/checkout@v6` | `token:` → effective token | -| `git push` | inherits from checkout | -| `gh release create` | `GH_TOKEN:` env → effective token | - -## Caller Usage Example - -```yaml -jobs: - release: - uses: nullplatform/actions-nullplatform/.github/workflows/changelog-release.yml@main - with: - project-type: npm - secrets: - app_id: ${{ secrets.APP_RELEASE_ID }} - app_private_key: ${{ secrets.APP_RELEASE_PRIVATE_KEY }} -``` - -## Backwards Compatibility - -Callers that do not pass `app_id` / `app_private_key` continue to work with `GITHUB_TOKEN` unchanged. From 908ed7ec514d9545a19a3938658e37cc03dfb58e Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Wed, 27 May 2026 10:25:29 -0300 Subject: [PATCH 3/3] feat(release): add optional GitHub App authentication to release workflows Adds app_id input and app_private_key secret to both release.yml and changelog-release.yml. When provided, a short-lived token is generated via actions/create-github-app-token@v3 to bypass branch protection rules on main. Falls back to GITHUB_TOKEN when not set. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/changelog-release.yml | 21 +++++++++++++++++-- .github/workflows/release.yml | 28 ++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/.github/workflows/changelog-release.yml b/.github/workflows/changelog-release.yml index f074b79..da58e9e 100644 --- a/.github/workflows/changelog-release.yml +++ b/.github/workflows/changelog-release.yml @@ -27,6 +27,15 @@ on: description: 'Commit message for version bump' type: string default: 'chore(release): bump version and update changelog [skip ci]' + app_id: + description: 'GitHub App ID for generating a token to bypass branch protections (optional)' + required: false + type: string + default: '' + secrets: + app_private_key: + description: 'GitHub App private key (required if app_id is set)' + required: false outputs: has_changes: description: 'Whether there were changes to release' @@ -48,11 +57,19 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: + - name: Generate GitHub App token + id: app-token + if: ${{ inputs.app_id != '' }} + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ inputs.app_id }} + private-key: ${{ secrets.app_private_key }} + - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} - name: Configure Git run: | @@ -396,7 +413,7 @@ jobs: - name: Create GitHub Release if: steps.changelog.outputs.has_changes == 'true' && inputs.create-github-release env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} CHANGELOG: ${{ steps.changelog.outputs.changelog }} run: | while IFS= read -r tag; do diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a8d5a5..3064652 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,15 @@ on: required: false type: boolean default: true + app_id: + description: 'GitHub App ID for generating a token to bypass branch protections (optional)' + required: false + type: string + default: '' + secrets: + app_private_key: + description: 'GitHub App private key (required if app_id is set)' + required: false permissions: contents: write @@ -28,6 +37,14 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: + - name: Generate GitHub App token + id: app-token + if: ${{ inputs.app_id != '' }} + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ inputs.app_id }} + private-key: ${{ secrets.app_private_key }} + - name: Checkout repository uses: actions/checkout@v6 @@ -36,6 +53,7 @@ jobs: uses: googleapis/release-please-action@v5 with: release-type: ${{ inputs.release-type }} + token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} update-readme-versions: name: Update README Versions @@ -45,11 +63,19 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: + - name: Generate GitHub App token + id: app-token + if: ${{ inputs.app_id != '' }} + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ inputs.app_id }} + private-key: ${{ secrets.app_private_key }} + - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} - name: Update all README versions run: |