diff --git a/.github/workflows/use-action.yaml b/.github/workflows/use-action.yaml index 5fdd517..b3e01d6 100644 --- a/.github/workflows/use-action.yaml +++ b/.github/workflows/use-action.yaml @@ -18,15 +18,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Read service account key - id: sa-key - run: echo "content=$(cat github-actions-metrics-key.json | jq -c)" >> $GITHUB_OUTPUT - - uses: ./ with: github-token: ${{ github.token }} gcp-project-id: jason-chainguard - gcp-service-account-key: ${{ steps.sa-key.outputs.content }} + gcp-service-account-key-file: github-actions-metrics-key.json - uses: actions/setup-node@v4 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..05ea400 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: local + hooks: + - id: npm-test + name: Run npm test + entry: npm test + language: system + pass_filenames: false + stages: [commit] + + - id: npm-build + name: Run npm build + entry: npm run build + language: system + pass_filenames: false + stages: [commit] diff --git a/README.md b/README.md index 6e3c0d4..ebfc01a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,11 @@ gcloud iam service-accounts keys create github-actions-metrics-key.json \ --iam-account="github-actions-metrics@${PROJECT_ID}.iam.gserviceaccount.com" ``` -### 2. Add Service Account Key to GitHub Secrets +### 2. Add Service Account Key to GitHub + +You have two options: + +**Option A: Use GitHub Secrets (Recommended for production)** 1. Copy the contents of the JSON key file: ```bash @@ -64,9 +68,34 @@ gcloud iam service-accounts keys create github-actions-metrics-key.json \ rm github-actions-metrics-key.json ``` +**Option B: Commit the key file (For testing/demo only)** + +If you're testing in a personal repository, you can commit the key file directly: + +```bash +git add github-actions-metrics-key.json +git commit -m "Add service account key for metrics" +git push +``` + +**⚠️ Warning:** Only do this in private repositories for testing. For production, always use secrets. + ## Usage -### Basic Usage +### Authentication Methods + +This action supports two authentication methods: + +| Method | Best For | Works With | Setup Complexity | +|--------|----------|------------|------------------| +| **Service Account Key File** | Testing, forks, `pull_request` events | All workflow events | Simple - just create and store a key | +| **Workload Identity Federation** | Production, security-conscious | `push`, `pull_request_target` only | Moderate - requires WIF setup | + +**Quick Decision:** +- Using `pull_request` from forks? → **Use Service Account Key File** +- Production workflows on `push`? → **Use Workload Identity Federation** + +### Basic Usage (with Service Account Key File) Add the action as one of the first steps in your workflow. The action will automatically collect metrics in a post-action phase after all other steps complete. @@ -80,16 +109,22 @@ jobs: runs-on: ubuntu-latest steps: + # Checkout first + - uses: actions/checkout@v4 + + # Write secret to file + - name: Setup GCP credentials + run: echo '${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}' > /tmp/gcp-key.json + # Enable metrics collection - name: Setup OpenTelemetry Metrics uses: your-org/otel-action@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }} gcp-project-id: ${{ secrets.GCP_PROJECT_ID }} - gcp-service-account-key: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY }} + gcp-service-account-key-file: /tmp/gcp-key.json # Your regular workflow steps - - uses: actions/checkout@v4 - name: Install dependencies run: npm install - name: Run tests @@ -100,6 +135,23 @@ jobs: # Metrics are automatically collected and exported after this job completes ``` +### Alternative: Using Committed Key File + +If you have the key file committed (for testing): + +```yaml +steps: + - uses: actions/checkout@v4 + + - uses: your-org/otel-action@v1 + with: + github-token: ${{ github.token }} + gcp-project-id: your-project-id + gcp-service-account-key-file: path/to/key.json + + # Your workflow steps... +``` + ### Advanced Configuration ```yaml @@ -108,7 +160,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} gcp-project-id: ${{ secrets.GCP_PROJECT_ID }} - gcp-service-account-key: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY }} + gcp-service-account-key-file: /tmp/gcp-key.json # Optional: Customize service name for resource attributes service-name: 'my-app-ci' @@ -120,7 +172,7 @@ jobs: metric-prefix: 'ci.metrics' # Optional: Adjust export interval (milliseconds) - export-interval-millis: '5000' + export-interval-millis: '1000' ``` ## Metrics Collected @@ -184,36 +236,74 @@ custom.googleapis.com/github.actions/step.total | rate(1m) ``` -## Alternative: Using Workload Identity Federation +## Alternative: Using Workload Identity Federation (Recommended) -Instead of service account keys, you can use Workload Identity Federation (recommended for production): +Instead of service account keys, you can use Workload Identity Federation (WIF). This is the **recommended** approach for production as it doesn't require managing service account key files. + +### Important: Workflow Event Limitations + +⚠️ **WIF requires `id-token: write` permission, which is only available for:** +- `push` events +- `pull_request_target` events (runs in the context of the target repo) + +❌ **WIF will NOT work with:** +- `pull_request` events (runs in the context of the fork, cannot get OIDC tokens) + +For pull requests from forks, you must use service account keys instead. + +### WIF Configuration ```yaml +name: CI with WIF + +on: + push: + branches: [ main ] + pull_request_target: # Use pull_request_target, not pull_request + branches: [ main ] + jobs: build: runs-on: ubuntu-latest permissions: contents: read - id-token: write + id-token: write # Required for WIF + actions: read # Required to read workflow/job info steps: - - name: Setup OpenTelemetry Metrics - uses: your-org/otel-action@v1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - gcp-project-id: ${{ secrets.GCP_PROJECT_ID }} - # No service account key needed! + - uses: actions/checkout@v4 + # Authenticate with GCP first - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.WIF_PROVIDER }} service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }} + # Setup metrics collection (no key file needed) + - uses: your-org/otel-action@v1 + with: + github-token: ${{ github.token }} + gcp-project-id: ${{ secrets.GCP_PROJECT_ID }} + # gcp-service-account-key-file is omitted - uses ADC from WIF + # Your workflow steps... + - run: npm install + - run: npm test ``` +### Setting Up Workload Identity Federation + +1. Create a Workload Identity Pool and Provider in GCP +2. Grant the service account permissions to the pool +3. Add secrets to GitHub: + - `WIF_PROVIDER`: Full resource name of the workload identity provider + - `WIF_SERVICE_ACCOUNT`: Email of the service account + - `GCP_PROJECT_ID`: Your GCP project ID + +See [Google's documentation](https://github.com/google-github-actions/auth#workload-identity-federation) for detailed setup instructions. + ## Development ### Install Dependencies @@ -240,7 +330,31 @@ This creates: - `dist/index.js` - Main entry point - `dist/post/index.js` - Post-action with all dependencies -**Important:** Always run `npm run build` before committing changes. The `dist/` directory must be committed for the action to work in GitHub Actions. +**Important:** The `dist/` directory must be committed for the action to work in GitHub Actions. + +### Pre-commit Hooks + +This project uses [pre-commit](https://pre-commit.com/) to automatically run tests and build before each commit. + +**Setup:** + +```bash +# Install pre-commit (if not already installed) +brew install pre-commit # macOS +# or: pip install pre-commit # other platforms + +# Install the git hook scripts +pre-commit install +``` + +Now `npm test` and `npm run build` will run automatically before each commit. If either fails, the commit will be aborted. + +**Manual execution:** + +```bash +# Run hooks on all files +pre-commit run --all-files +``` ### Project Structure diff --git a/action.yml b/action.yml index c4cec3f..ae27052 100644 --- a/action.yml +++ b/action.yml @@ -12,8 +12,8 @@ inputs: gcp-project-id: description: 'Google Cloud Project ID where metrics will be exported' required: true - gcp-service-account-key: - description: 'Google Cloud service account key JSON (alternative to ADC/Workload Identity)' + gcp-service-account-key-file: + description: 'Path to Google Cloud service account key JSON file (alternative to ADC/Workload Identity)' required: false service-name: description: 'Service name for OpenTelemetry resource attributes' diff --git a/dist/post/index.js b/dist/post/index.js index de3ba0e..b1c6491 100644 --- a/dist/post/index.js +++ b/dist/post/index.js @@ -109,21 +109,36 @@ module.exports = { collectMetrics }; /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const core = __nccwpck_require__(37484); +const fs = __nccwpck_require__(79896); +const path = __nccwpck_require__(16928); /** * Parses and validates action configuration from inputs * @returns {Object} Configuration object */ function getConfig() { - const serviceAccountKey = core.getInput('gcp-service-account-key'); + const serviceAccountKeyFile = core.getInput('gcp-service-account-key-file'); + let serviceAccountKey = null; + + // Read service account key from file if provided + if (serviceAccountKeyFile) { + try { + const filePath = path.resolve(serviceAccountKeyFile); + core.info(`Reading service account key from: ${filePath}`); + serviceAccountKey = fs.readFileSync(filePath, 'utf8'); + } catch (error) { + core.error(`Failed to read service account key file: ${error.message}`); + throw new Error(`Cannot read service account key file: ${serviceAccountKeyFile}`); + } + } const config = { gcpProjectId: core.getInput('gcp-project-id', { required: true }), - gcpServiceAccountKey: serviceAccountKey || null, + gcpServiceAccountKey: serviceAccountKey, serviceName: core.getInput('service-name') || 'github-actions', serviceNamespace: core.getInput('service-namespace') || 'ci', metricPrefix: core.getInput('metric-prefix') || 'github.actions', - exportIntervalMillis: parseInt(core.getInput('export-interval-millis') || '5000', 10), + exportIntervalMillis: parseInt(core.getInput('export-interval-millis') || '1000', 10), }; // Validate configuration diff --git a/lib/config.js b/lib/config.js index 1780916..d9ce939 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,19 +1,34 @@ const core = require('@actions/core'); +const fs = require('fs'); +const path = require('path'); /** * Parses and validates action configuration from inputs * @returns {Object} Configuration object */ function getConfig() { - const serviceAccountKey = core.getInput('gcp-service-account-key'); + const serviceAccountKeyFile = core.getInput('gcp-service-account-key-file'); + let serviceAccountKey = null; + + // Read service account key from file if provided + if (serviceAccountKeyFile) { + try { + const filePath = path.resolve(serviceAccountKeyFile); + core.info(`Reading service account key from: ${filePath}`); + serviceAccountKey = fs.readFileSync(filePath, 'utf8'); + } catch (error) { + core.error(`Failed to read service account key file: ${error.message}`); + throw new Error(`Cannot read service account key file: ${serviceAccountKeyFile}`); + } + } const config = { gcpProjectId: core.getInput('gcp-project-id', { required: true }), - gcpServiceAccountKey: serviceAccountKey || null, + gcpServiceAccountKey: serviceAccountKey, serviceName: core.getInput('service-name') || 'github-actions', serviceNamespace: core.getInput('service-namespace') || 'ci', metricPrefix: core.getInput('metric-prefix') || 'github.actions', - exportIntervalMillis: parseInt(core.getInput('export-interval-millis') || '5000', 10), + exportIntervalMillis: parseInt(core.getInput('export-interval-millis') || '1000', 10), }; // Validate configuration