diff --git a/README.md b/README.md index cf3f549..7c9b987 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ | Passing data between steps and jobs | [passing-data.md](./modules/02-github-actions/passing-data.md) | | Reusable workflows & composite actions | [reusable-workflows.md](./modules/02-github-actions/reusable-workflows.md) | | Performance & scale: caching & matrix builds | [caching-and-matrix.md](./modules/02-github-actions/caching-and-matrix.md) | +| Marketplace actions: find, evaluate, use safely | [marketplace-actions.md](./modules/02-github-actions/marketplace-actions.md) | | Security best practices & SHA pinning | [security-best-practices.md](./modules/02-github-actions/security-best-practices.md) | | `GITHUB_TOKEN` & least-privilege permissions | [10-permissions.md](./modules/02-github-actions/10-permissions.md) | | Runners: hosted vs self-hosted | [runners-guide.md](./modules/02-github-actions/runners-guide.md) | diff --git a/modules/02-github-actions/exercises/exercise-3.md b/modules/02-github-actions/exercises/exercise-3.md index 9207f63..3efe155 100644 --- a/modules/02-github-actions/exercises/exercise-3.md +++ b/modules/02-github-actions/exercises/exercise-3.md @@ -90,9 +90,9 @@ steps: | # | TODO | Concept | Read this | |---|---|---|---| -| ① | `runs.using: "composite"` | Composite action declaration | [reusable-workflows.md → "Composite Actions"](../reusable-workflows.md?plain=1#L102) | -| ② | Use input in composite step | `${{ inputs. }}` in actions | [reusable-workflows.md → composite example](../reusable-workflows.md?plain=1#L106) | -| ③ | `shell: bash` for run steps | Composite action requirement | [reusable-workflows.md → composite example](../reusable-workflows.md?plain=1#L140) | +| ① | `runs.using: "composite"` | Composite action declaration | [reusable-workflows.md → composite `runs:` block](../reusable-workflows.md?plain=1#L126) | +| ② | Use input in composite step | `${{ inputs. }}` in actions | [reusable-workflows.md → `${{ inputs.python-version }}` line](../reusable-workflows.md?plain=1#L131) | +| ③ | `shell: bash` for run steps | Composite action requirement | [reusable-workflows.md → `shell: bash` line](../reusable-workflows.md?plain=1#L140) | → Solution: [solutions/03-release-workflow/.github/actions/setup-python-project/action.yml](../../../solutions/03-release-workflow/.github/actions/setup-python-project/action.yml) @@ -155,9 +155,9 @@ jobs: | # | TODO | Concept | Read this | |---|---|---|---| -| ④ | `on: workflow_call:` | Reusable workflow trigger | [reusable-workflows.md → "Reusable Workflows"](../reusable-workflows.md?plain=1#L21) | -| ⑤ | `fromJSON()` to expand matrix | Dynamic matrix from input string | [caching-and-matrix.md → "Dynamic matrix from a workflow input"](../caching-and-matrix.md?plain=1#L135) | -| ⑥ | Local action via `uses: ./path` | Calling a composite action | [reusable-workflows.md → "Use the composite action"](../reusable-workflows.md?plain=1#L143) | +| ④ | `on: workflow_call:` | Reusable workflow trigger | [reusable-workflows.md → `on: workflow_call:` block](../reusable-workflows.md?plain=1#L29) | +| ⑤ | `fromJSON()` to expand matrix | Dynamic matrix from input string | [caching-and-matrix.md → `fromJSON(inputs.python-versions)` line](../caching-and-matrix.md?plain=1#L154) | +| ⑥ | Local action via `uses: ./path` | Calling a composite action | [reusable-workflows.md → `uses: ./.github/actions/...` line](../reusable-workflows.md?plain=1#L150) | → Solution: [solutions/03-release-workflow/.github/workflows/reusable-validate.yml](../../../solutions/03-release-workflow/.github/workflows/reusable-validate.yml) @@ -249,10 +249,10 @@ jobs: | # | TODO | Concept | Read this | |---|---|---|---| -| ⑦ | `workflow_dispatch` inputs | Manual triggers | [manual-triggers.md → "Inputs"](../manual-triggers.md?plain=1#L20) | -| ⑧ | `jobs..uses:` | Calling a reusable workflow | [reusable-workflows.md → "Call the reusable workflow"](../reusable-workflows.md?plain=1#L64) | -| ⑨ | Reusing the composite action | DRY principle | [reusable-workflows.md → "When to Use Which"](../reusable-workflows.md?plain=1#L159) | -| ⑩ | Third-party action via SHA-pinned `uses:` | Marketplace actions | [security-best-practices.md → "SHA Pin Third-Party Actions"](../security-best-practices.md?plain=1#L7) | +| ⑦ | `workflow_dispatch` inputs | Manual triggers | [manual-triggers.md → `workflow_dispatch:` snippet](../manual-triggers.md?plain=1#L23) | +| ⑧ | `jobs..uses:` | Calling a reusable workflow | [reusable-workflows.md → `uses: ./.github/workflows/deploy.yml` line](../reusable-workflows.md?plain=1#L75) | +| ⑨ | Reusing the composite action | DRY principle | [reusable-workflows.md → `uses: ./.github/actions/...` line](../reusable-workflows.md?plain=1#L150) | +| ⑩ | Third-party action via SHA-pinned `uses:` | Marketplace actions | [marketplace-actions.md → `Create GitHub Release` step (copy this block)](../marketplace-actions.md?plain=1#L71) · [security-best-practices.md → SHA-pin example](../security-best-practices.md?plain=1#L16) | → Solution: [solutions/03-release-workflow/.github/workflows/release.yml](../../../solutions/03-release-workflow/.github/workflows/release.yml) diff --git a/modules/02-github-actions/marketplace-actions.md b/modules/02-github-actions/marketplace-actions.md new file mode 100644 index 0000000..fa311dc --- /dev/null +++ b/modules/02-github-actions/marketplace-actions.md @@ -0,0 +1,120 @@ +# GitHub Actions Marketplace + +> **TL;DR** — The Marketplace is GitHub's directory of pre-built actions you can drop into a workflow with one `uses:` line. Treat third-party actions like third-party code: verify, pin to a SHA, and review what permissions they request. + +The vast majority of CI/CD steps you'd otherwise write by hand already exist as a Marketplace action. Need to publish a release? `softprops/action-gh-release`. Authenticate to AWS? `aws-actions/configure-aws-credentials`. Run Trivy? `aquasecurity/trivy-action`. + +--- + +## Finding an action + +Browse: https://github.com/marketplace?type=actions + +```bash +# Search from the CLI +gh api -X GET search/repositories \ + -f q='topic:github-action release publish' \ + --jq '.items[] | {full_name, stargazers_count, html_url}' | head -20 +``` + +When you read an action's listing, look for: + +| Signal | What you want | +|--------|---------------| +| **Verified creator** badge | ✅ official org (e.g. `actions/`, `aws-actions/`, `docker/`) | +| **Stars / installs** | High = battle-tested | +| **Last commit / release** | Recent (< 6 months) | +| **Open issues** | Triaged, not piling up | +| **`action.yml`** | Read it. It's small. You can audit the whole thing. | +| **Required permissions** | The README usually lists them — minimum needed | + +--- + +## Using an action + +```yaml +- name: Create GitHub Release + uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 + with: + tag_name: ${{ needs.resolve.outputs.version }} + name: Release ${{ needs.resolve.outputs.version }} + generate_release_notes: true + prerelease: ${{ needs.resolve.outputs.prerelease == 'true' }} + files: dist/* +``` + +Three rules: + +1. **Pin to a SHA.** Tags are mutable; SHAs aren't. See [security-best-practices.md → "SHA Pin Third-Party Actions"](./security-best-practices.md?plain=1#L7). +2. **Read its `action.yml`.** Inputs, outputs, and required permissions are all in one short file in the action's repo. +3. **Grant only the permissions it needs.** The release example above needs `permissions: contents: write` — granted at the *job* level only, not workflow-wide. + +--- + +## Example: publish a GitHub Release + +This is the pattern the release-pipeline exercise (TODO ⑩) builds toward. + +```yaml +publish: + needs: [build] + runs-on: ubuntu-latest + permissions: + contents: write # ← only this job can create a release + steps: + - name: Download dist + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: dist + path: dist/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 + with: + tag_name: ${{ inputs.version }} + name: Release ${{ inputs.version }} + generate_release_notes: true # auto-generate from PR titles + prerelease: ${{ inputs.prerelease }} # honor the dispatch input + files: dist/* # attach build artifacts +``` + +What you get: a new entry on the repo's **Releases** page, the resolved version as a Git tag, auto-generated notes built from PR titles since the previous tag, and every file under `dist/` attached as a downloadable asset. + +--- + +## Other commonly-used Marketplace actions + +| Need | Action | +|------|--------| +| Checkout code | [`actions/checkout`](https://github.com/marketplace/actions/checkout) (first-party) | +| Setup Python | [`actions/setup-python`](https://github.com/marketplace/actions/setup-python) (first-party) | +| Cache deps | [`actions/cache`](https://github.com/marketplace/actions/cache) (first-party) | +| Upload/download artifacts | [`actions/upload-artifact`](https://github.com/marketplace/actions/upload-a-build-artifact) (first-party) | +| Build & push Docker images | [`docker/build-push-action`](https://github.com/marketplace/actions/build-and-push-docker-images) | +| Authenticate to AWS | [`aws-actions/configure-aws-credentials`](https://github.com/marketplace/actions/configure-aws-credentials-action-for-github-actions) | +| Authenticate to Azure | [`azure/login`](https://github.com/marketplace/actions/azure-login) | +| Publish a GitHub Release | [`softprops/action-gh-release`](https://github.com/marketplace/actions/gh-release) | +| Send a Slack notification | [`slackapi/slack-github-action`](https://github.com/marketplace/actions/slack-send) | +| Container vulnerability scan | [`aquasecurity/trivy-action`](https://github.com/marketplace/actions/aqua-security-trivy) | +| Dependency review | [`actions/dependency-review-action`](https://github.com/marketplace/actions/dependency-review) (first-party) | + +--- + +## When *not* to use a Marketplace action + +Sometimes a 3-line `run:` is better than a third-party dependency: + +```yaml +# Don't pull in an action for this: +- run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" + +# Don't pull in an action for this either: +- run: | + if [ "$ENV" = "prod" ]; then ./deploy.sh; fi +``` + +Rule of thumb: if the action is < 30 lines of shell, just inline it. Every dependency is a supply-chain risk. + +--- + +→ Next: [security-best-practices.md](./security-best-practices.md) for SHA pinning and least-privilege patterns. diff --git a/resources/presentation/github-pr-ui.png b/resources/presentation/github-pr-ui.png new file mode 100644 index 0000000..9aac5fd Binary files /dev/null and b/resources/presentation/github-pr-ui.png differ