diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1ef2d0c..d4899ef 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,6 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 25 + commit-message: + prefix: "chore" + include: "scope" diff --git a/.github/release-please-config.json b/.github/release-please-config.json new file mode 100644 index 0000000..ae159fd --- /dev/null +++ b/.github/release-please-config.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "separator": "/", + "include-component-in-tag": true, + "include-v-in-tag": true, + "release-type": "simple", + "skip-github-release": true, + "changelog-path": "CHANGELOG.md", + "packages": { + ".github/workflows/build_container_image": { + "component": "build_container_image", + "include-paths": [".github/workflows/build_container_image.yml"] + }, + ".github/workflows/build_node_package": { + "component": "build_node_package", + "include-paths": [".github/workflows/build_node_package.yml"] + }, + ".github/workflows/build_node_package_with_pgsql": { + "component": "build_node_package_with_pgsql", + "include-paths": [".github/workflows/build_node_package_with_pgsql.yml"] + }, + ".github/workflows/deploy_container_image": { + "component": "deploy_container_image", + "include-paths": [".github/workflows/deploy_container_image.yml"] + }, + ".github/workflows/deploy_helm_chart": { + "component": "deploy_helm_chart", + "include-paths": [".github/workflows/deploy_helm_chart.yml"] + }, + ".github/workflows/merge_multiarch_images": { + "component": "merge_multiarch_images", + "include-paths": [".github/workflows/merge_multiarch_images.yml"] + }, + ".github/workflows/publish_node_package": { + "component": "publish_node_package", + "include-paths": [".github/workflows/publish_node_package.yml"] + }, + ".github/workflows/sast_scan": { + "component": "sast_scan", + "include-paths": [".github/workflows/sast_scan.yaml"] + }, + "actions/npm_test": { + "component": "npm_test" + }, + "actions/scan_container_image": { + "component": "scan_container_image" + }, + "actions/update-nr-flows": { + "component": "update-nr-flows" + } + } +} diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json new file mode 100644 index 0000000..953a5c8 --- /dev/null +++ b/.github/release-please-manifest.json @@ -0,0 +1,13 @@ +{ + ".github/workflows/build_container_image": "1.0.0", + ".github/workflows/build_node_package": "1.0.0", + ".github/workflows/build_node_package_with_pgsql": "1.0.0", + ".github/workflows/deploy_container_image": "1.0.0", + ".github/workflows/deploy_helm_chart": "1.0.0", + ".github/workflows/merge_multiarch_images": "1.0.0", + ".github/workflows/publish_node_package": "1.0.0", + ".github/workflows/sast_scan": "1.0.0", + "actions/npm_test": "1.0.0", + "actions/scan_container_image": "1.0.0", + "actions/update-nr-flows": "1.0.0" +} diff --git a/.github/workflows/autotag.yml b/.github/workflows/autotag.yml deleted file mode 100644 index 997d158..0000000 --- a/.github/workflows/autotag.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Bump reusable workflows version -on: - pull_request: - types: - - closed - branches: - - main - workflow_dispatch: - -jobs: - bump-version: - name: Bump version - if: github.event.pull_request.merged == true - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout the code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.pull_request.merge_commit_sha }} - fetch-depth: '0' - - - name: Bump version and push tag - uses: anothrNick/github-tag-action@4ed44965e0db8dab2b466a16da04aec3cc312fd8 # 1.75.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - WITH_V: true diff --git a/.github/workflows/lint-pr-title.yaml b/.github/workflows/lint-pr-title.yaml new file mode 100644 index 0000000..ccc444d --- /dev/null +++ b/.github/workflows/lint-pr-title.yaml @@ -0,0 +1,16 @@ +name: Lint Pull Request Title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml new file mode 100644 index 0000000..244ad73 --- /dev/null +++ b/.github/workflows/release-please.yaml @@ -0,0 +1,52 @@ +name: Prepare release + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - name: Run Release Please + id: rp + uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + with: + config-file: .github/release-please-config.json + manifest-file: .github/release-please-manifest.json + + - name: Checkout + if: steps.rp.outputs.releases_created == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Fast-forward major/minor floats + if: steps.rp.outputs.releases_created == 'true' + env: + RP_OUTPUTS: ${{ toJSON(steps.rp.outputs) }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + echo "$RP_OUTPUTS" | jq -r ' + (.paths_released | fromjson) as $paths + | $paths[] as $p + | "\(.[$p + "--tag_name"])\t\(.[$p + "--sha"])" + ' | while IFS=$'\t' read -r TAG SHA; do + COMP="${TAG%/*}" + VER="${TAG##*/v}" + MAJ="${VER%%.*}" + REST="${VER#*.}" + MIN="${REST%%.*}" + echo "Fast-forwarding ${COMP}/v${MAJ} and ${COMP}/v${MAJ}.${MIN} -> ${SHA}" + git tag -f "${COMP}/v${MAJ}" "${SHA}" + git tag -f "${COMP}/v${MAJ}.${MIN}" "${SHA}" + git push origin -f "${COMP}/v${MAJ}" "${COMP}/v${MAJ}.${MIN}" + done diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/README.md b/README.md index c110be8..e364928 100644 --- a/README.md +++ b/README.md @@ -1 +1,130 @@ -General purpose reusable Github Action workflows +# github-actions-workflows + +General purpose reusable GitHub Action workflows and composite actions. + +## Versioning and Release Process + +Each reusable workflow and composite action in this repository is versioned and released **independently** using [Release Please](https://github.com/googleapis/release-please). The next version of each component is determined automatically from the [Conventional Commits](https://www.conventionalcommits.org/) in pull request titles that touched the component's files. + +### Components + +1. The `Prepare release` GitHub Action workflow (`.github/workflows/release-please.yaml`): + + * Runs `release-please-action` on every push to `main`. Release Please analyzes commit messages since the last release and groups pending version bumps into a single, rolling release pull request. + * The release PR lists one entry per component that has unreleased changes, each with its proposed new version and changelog excerpt. + * Merging the release PR creates the exact version tag for each released component (e.g. `build_container_image/v1.2.3`) and writes the aggregated entries into the repo-root `CHANGELOG.md`. + * After the exact tags are created, the same workflow fast-forwards the per-component **floating tags** - the major float `/vX` and the minor float `/vX.Y` - so they point at the same commit. Consumers pinned to a float automatically pick up the new release on their next run. + +2. The `Lint Pull Request Title` GitHub Action workflow (`.github/workflows/lint-pr-title.yaml`): + + * Runs on every pull request and uses the `amannn/action-semantic-pull-request` action to validate that the pull request title follows the Conventional Commits format. + * This repository squash-merges all pull requests, so the PR title becomes the squash commit's subject line. The lint gate therefore ensures that Release Please can parse every commit merged into `main`. + +### Pull Request Title Format + +The Conventional Commits preset expects pull request titles to be in the following format: + + (): + +* Type: Describes the category of the commit. Examples include: + * `feat`: A new feature (triggers a minor version bump). + * `fix`: A bug fix (triggers a patch version bump). + * `perf`: A code change that improves performance (triggers a patch version bump). + * `refactor`: A code change that neither fixes a bug nor adds a feature (triggers a patch version bump unless it is a BREAKING CHANGE). + * `docs`: Documentation-only changes. + * `chore`: Routine maintenance (e.g. CI tweaks, dependency bumps). +* Scope: An optional part that provides additional context about what was changed (e.g. module, component). +* Subject: A brief description of the changes. + +A commit whose files fall within a component's tracked paths always produces at least a patch bump for that component, regardless of type. Types like `chore` and `docs` are hidden from the visible `CHANGELOG.md` but still participate in version calculation. + +### Handling Breaking Changes + +To indicate a breaking change, the exclamation mark `!` should be used immediately after the type (or type and scope): + +* `feat!:` +* `fix!:` +* `refactor!:` + +A breaking commit bumps the affected components to a new major version. Consumers pinned to the previous major float (e.g. `@build_container_image/v1`) stay frozen on the old major line — picking up the new major requires re-pinning to the new float explicitly. + +### Tag Scheme + +For every release, three tags are produced per affected component: + +| Tag | Mutability | Points at | +|------------------------|-------------------------------------|-----------------------------------| +| `/vX.Y.Z` | immutable | The release commit | +| `/vX.Y` | moves on patch releases | Latest patch of `vX.Y.x` | +| `/vX` | moves on any non-breaking release | Latest non-breaking of `vX.x.y` | + +Consumers reference a component by one of these tags: + +```yaml +# Major float — auto-updates on every non-breaking release (recommended): +uses: FlowFuse/github-actions-workflows/.github/workflows/build_container_image.yml@build_container_image/v1 + +# Minor float — auto-updates only on patches: +uses: FlowFuse/github-actions-workflows/.github/workflows/build_container_image.yml@build_container_image/v1.2 + +# Exact pin — never moves: +uses: FlowFuse/github-actions-workflows/.github/workflows/build_container_image.yml@build_container_image/v1.2.3 + +# Composite actions use the same pattern: +uses: FlowFuse/github-actions-workflows/actions/npm_test@npm_test/v1 +``` + +### Adding a New Reusable Workflow or Composite Action + +When introducing a new component, it must be registered with Release Please so that it is versioned and tagged independently like the existing ones. + +1. **Create the workflow or action file.** + + * Reusable workflow: place the YAML file at `.github/workflows/.yml`. + * Composite action: create the directory `actions//` and add its `action.yml` (plus any supporting files). + * `` must be unique across **all** workflows and actions — the two share a flat tag namespace. + +2. **Register the component in `.github/release-please-config.json`** by adding a new entry under `packages`. + + * For a reusable workflow: + + ```json + ".github/workflows/": { + "component": "", + "include-paths": [".github/workflows/.yml"] + } + ``` + + The package key is a virtual path (no directory is created on disk); `include-paths` scopes change detection to the single YAML file. + + * For a composite action: + + ```json + "actions/": { + "component": "" + } + ``` + + The package key is the action's real directory; `include-paths` is not needed because any file under that directory is attributed to the component. + +3. **Seed the initial version in `.github/release-please-manifest.json`** using the same key chosen in the previous step: + + ```json + "": "1.0.0" + ``` + +4. **Open the pull request with a Conventional Commit title**, for example `feat: add reusable workflow`. + +5. **After merging the pull request, bootstrap the initial tags** for the new component. They must exist before consumers can reference it and before Release Please runs cleanly for the new entry: + + ```bash + git fetch origin main + SHA=$(git rev-parse origin/main) + NAME="" + git tag "${NAME}/v1.0.0" "${SHA}" + git tag "${NAME}/v1.0" "${SHA}" + git tag "${NAME}/v1" "${SHA}" + git push origin "${NAME}/v1.0.0" "${NAME}/v1.0" "${NAME}/v1" + ``` + + > If Release Please happens to run between the merge and this bootstrap (it triggers on every push to `main`), it may open a stray release pull request proposing an inflated first version for the new component, because it has no tag to use as a baseline. Simply close that pull request — the next Release Please run, once the bootstrap tags exist, will be a no-op for the new component.