Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/github-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Reusable workflow that publishes a GitHub release or pre-release of a version, tagging the commit
# the run is for. A pre-release replaces the one of the same name, so a pull request can publish its
# X.Y.Z-PRE-<number> on every push; a release never replaces one. See README.md for how to call it.
name: GitHub release

on:
workflow_call:
inputs:
version:
description: Tag and name of the release, e.g. the version output of semver-version.yml
type: string
required: true
prerelease:
description: Publish a pre-release
type: boolean
default: false
title:
description: Title of the release (default the version)
type: string
default: ""
target:
description: Commit to tag (default the commit the run is for)
type: string
default: ""
files:
description: Newline-separated globs of the files to attach, relative to the repository root
type: string
default: ""
artifact-pattern:
description: Artifacts of the same run to download into release-assets/ before attaching the files
type: string
default: ""

jobs:
release:
name: GitHub release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ inputs.target || github.sha }}
persist-credentials: false

# the script from the same commit as this workflow, not from the caller's repository
- name: Locate this workflow
env:
WORKFLOW_REPOSITORY: ${{ job.workflow_repository }}
WORKFLOW_SHA: ${{ job.workflow_sha }}
run: |
if [ -z "$WORKFLOW_REPOSITORY" ] || [ -z "$WORKFLOW_SHA" ]; then
echo "::error::job.workflow_repository and job.workflow_sha are empty: this workflow has to run as a reusable workflow on github.com"
exit 1
fi
echo "Using $WORKFLOW_REPOSITORY@$WORKFLOW_SHA"

- name: Check out the release script
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
path: .github-workflows
persist-credentials: false

- name: Download the artifacts to attach
if: inputs.artifact-pattern != ''
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
path: release-assets
pattern: ${{ inputs.artifact-pattern }}
merge-multiple: true

- name: Publish the release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
TARGET: ${{ inputs.target || github.sha }}
PRERELEASE: ${{ inputs.prerelease }}
TITLE: ${{ inputs.title }}
FILES: ${{ inputs.files }}
run: .github-workflows/scripts/github-release.sh
53 changes: 36 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ concurrency:
group: release
cancel-in-progress: false

permissions:
contents: write

jobs:
release:
version:
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
major: ${{ steps.version.outputs.major }}
target: ${{ steps.version.outputs.target }}
steps:
- name: Check out the merged commit
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
Expand Down Expand Up @@ -66,24 +69,40 @@ jobs:
echo "::error::v$version already exists"
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "major=v${version%%.*}" >> "$GITHUB_OUTPUT"
{
echo "version=v$version"
echo "major=v${version%%.*}"
echo "target=$(git rev-parse HEAD)"
} >> "$GITHUB_OUTPUT"
echo "Releasing v$version"

- name: Tag the release
# the release workflow this repository offers, from this very commit
release:
needs: version
uses: ./.github/workflows/github-release.yml
permissions:
contents: write
with:
version: ${{ needs.version.outputs.version }}
target: ${{ needs.version.outputs.target }}

major-tag:
needs: [version, release]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out the merged commit
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ needs.version.outputs.target }}

- name: Move the major tag to the release
env:
VERSION: v${{ steps.version.outputs.version }}
MAJOR: ${{ steps.version.outputs.major }}
VERSION: ${{ needs.version.outputs.version }}
MAJOR: ${{ needs.version.outputs.major }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$VERSION" -m "$VERSION"
git tag -f -a "$MAJOR" -m "$MAJOR, currently $VERSION"
git push origin "refs/tags/$VERSION"
git push --force origin "refs/tags/$MAJOR"

- name: Publish the GitHub release
env:
GH_TOKEN: ${{ github.token }}
VERSION: v${{ steps.version.outputs.version }}
run: gh release create "$VERSION" --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes --title "$VERSION"
28 changes: 9 additions & 19 deletions .github/workflows/semver-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ on:
description: Text the release tags start with (default v); empty for tags like 1.2.3
type: string
default: v
default-bump:
description: >-
major, minor or patch for a push that no labelled pull request carries; empty (the default)
fails such a push instead
type: string
default: ""
outputs:
version:
description: X.Y.Z on a push, X.Y.Z-PRE-<pull request number> on a pull request
Expand Down Expand Up @@ -62,30 +68,14 @@ jobs:
# chart-pr.yml). On a push, the labels of the pull request the pushed commit was merged from.
- name: Read the semver label
id: label
working-directory: repo
env:
GH_TOKEN: ${{ github.token }}
EVENT: ${{ github.event_name }}
PR: ${{ github.event.pull_request.number }}
SHA: ${{ github.sha }}
run: |
if [ "$EVENT" = pull_request ]; then
labels=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq '[.labels[].name]')
else
pr=$(gh api "repos/$GITHUB_REPOSITORY/commits/$SHA/pulls" \
--jq '[.[] | select(.merged_at != null)][0] // empty | {number, labels: [.labels[].name]}')
if [ -z "$pr" ]; then
echo "::error::No merged pull request contains $SHA, so there is no semver label to release it with"
exit 1
fi
echo "Releasing pull request #$(jq -r .number <<< "$pr")"
labels=$(jq -c .labels <<< "$pr")
fi
bumps=$(jq -r '[.[] | select(. == "major" or . == "minor" or . == "patch")] | join(" ")' <<< "$labels")
if [ "$(wc -w <<< "$bumps")" -ne 1 ]; then
echo "::error::The pull request needs exactly one of the labels major, minor or patch (found: ${bumps:-none})"
exit 1
fi
echo "bump=$bumps" >> "$GITHUB_OUTPUT"
DEFAULT_BUMP: ${{ inputs.default-bump }}
run: ../semver-workflows/scripts/semver-label.sh

- name: Ask for the label
if: failure() && steps.label.outcome == 'failure' && github.event_name == 'pull_request'
Expand Down
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Reusable GitHub Actions workflows shared by FIWARE repositories, so that a fix i
| [`chart-release.yml`](#chart-release-chart-releaseyml) | Publishes the charts, failing when a changed chart has no new version | Push to the main branch |
| [`chart-dependencies.yml`](#chart-dependencies-chart-dependenciesyml) | Opens pull requests for newer chart dependencies | Schedule |
| [`semver-version.yml`](#semver-version-semver-versionyml) | Works out the version to release from the semver label of the pull request | Pull requests and push to the main branch |
| [`github-release.yml`](#github-release-github-releaseyml) | Publishes a GitHub release or pre-release of a version | Pull requests and push to the main branch |
| [`stale.yml`](#stale-issues-staleyml) | Marks and closes issues and pull requests without activity | Schedule |

Together, the chart workflows cover the life of a chart: a pull request is linted and gets its new version, and merging it publishes that version.
Expand Down Expand Up @@ -145,7 +146,7 @@ The chart version is bumped in the pull request itself because pull requests ope
For repositories that release an application, not charts, with the version driven by the semver label of the pull request. It works out the version and leaves the build and the release to the caller:

- on a pull request, it requires exactly one of the labels `major`, `minor` or `patch` (read when the job runs, as in `chart-pr.yml`), commenting on the pull request when it is missing, and returns `X.Y.Z-PRE-<pull request number>`;
- on a push to the main branch, it reads the label of the pull request the pushed commit was merged from, and returns `X.Y.Z`. A commit that no merged pull request contains, or one merged without a label, fails.
- on a push to the main branch, it reads the label of the pull request the pushed commit was merged from, and returns `X.Y.Z`. A commit that no merged pull request contains, or one merged without a label, fails, unless `default-bump` says how to release it.

`X.Y.Z` is the highest release tag bumped by the label. Release tags are `<tag-prefix>X.Y.Z`; other tags, such as pre-releases (`1.2.3-PRE-4`), are ignored.

Expand Down Expand Up @@ -173,12 +174,53 @@ jobs:
| Input | Default | Description |
|---|---|---|
| `tag-prefix` | `v` | Text the release tags start with; empty for tags like `1.2.3` |
| `default-bump` | (none) | `major`, `minor` or `patch` for a push that no labelled pull request carries; without it, such a push fails |

| Output | Description |
|---|---|
| `version` | `X.Y.Z` on a push, `X.Y.Z-PRE-<pull request number>` on a pull request |
| `bump` | `major`, `minor` or `patch`, from the label |

## GitHub release (`github-release.yml`)

Publishes a GitHub release of a version, tagging the commit the run is for, with notes generated from the merged pull requests. It pairs with `semver-version.yml`:

- a **pre-release** replaces the release and the tag of the same name, so a pull request publishes its `X.Y.Z-PRE-<number>` again on every push;
- a **release** never replaces one: a version that already exists fails the job.

```yaml
jobs:
generate-version:
uses: FIWARE/github-workflows/.github/workflows/semver-version.yml@v1
permissions:
contents: read
pull-requests: write
with:
tag-prefix: ""

git-release:
needs: [generate-version, build]
uses: FIWARE/github-workflows/.github/workflows/github-release.yml@v1
permissions:
contents: write
with:
version: ${{ needs.generate-version.outputs.version }}
prerelease: ${{ github.event_name == 'pull_request' }}
files: |
LICENSE
release-assets/my-binary-*
artifact-pattern: my-binary-* # artifacts of this run, downloaded into release-assets/
```

| Input | Default | Description |
|---|---|---|
| `version` | (required) | Tag and name of the release |
| `prerelease` | `false` | Publish a pre-release |
| `title` | the version | Title of the release |
| `target` | the commit of the run | Commit to tag |
| `files` | (none) | Newline-separated globs of the files to attach, relative to the repository root; a glob that matches nothing fails |
| `artifact-pattern` | (none) | Artifacts of the same run to download into `release-assets/` first |

## Stale issues (`stale.yml`)

Marks issues and pull requests without activity as stale, and closes them when they stay so, with [actions/stale](https://github.com/actions/stale).
Expand Down Expand Up @@ -211,8 +253,8 @@ Callers use the `v1` tag, which moves with every compatible release (`v1.0.1`, `
Every pull request to this repository needs one of the labels `major`, `minor` or `patch`. When it is merged, `release.yml`:

1. takes the highest `vX.Y.Z` tag and bumps it by the label;
2. tags the merge commit with the new `vX.Y.Z` and moves the major tag (`vX`) to it. A `major` release creates the next major tag and leaves the previous one where it was, so its callers keep working;
3. publishes a GitHub release with notes generated from the merged pull requests.
2. publishes the GitHub release `vX.Y.Z` of the merge commit with this repository's own `github-release.yml`, so every release also exercises it;
3. moves the major tag (`vX`) to it. A `major` release creates the next major tag and leaves the previous one where it was, so its callers keep working.

A pull request merged without a label is not released, and the workflow fails saying so. The first release, or any release no pull request carries, is made by running the Release workflow by hand on `main` with the version (`X.Y.Z`).

Expand Down
78 changes: 78 additions & 0 deletions scripts/github-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# Publishes a GitHub release, tagging TARGET with VERSION.
#
# A pre-release replaces the release and the tag of the same name, if there are any: a pull request
# publishes the same X.Y.Z-PRE-<number> on every push. A release refuses to replace one: a version,
# once released, does not change. The release notes are generated from the merged pull requests.
#
# Environment:
# VERSION tag and name of the release (required)
# TARGET commit to tag (required)
# PRERELEASE true for a pre-release (default: false)
# TITLE title of the release (default: VERSION)
# FILES newline- or space-separated globs of the files to attach, relative to the current
# directory; a glob that matches nothing fails
# REPOSITORY owner/repo (default: $GITHUB_REPOSITORY)
# GH gh binary (default: gh)

set -euo pipefail

: "${VERSION:?VERSION must name the release}"
: "${TARGET:?TARGET must name the commit to tag}"
PRERELEASE="${PRERELEASE:-false}"
TITLE="${TITLE:-$VERSION}"
FILES="${FILES:-}"
REPOSITORY="${REPOSITORY:-${GITHUB_REPOSITORY:?REPOSITORY or GITHUB_REPOSITORY must name the repository}}"
GH="${GH:-gh}"

case "$PRERELEASE" in
true|false) ;;
*) echo "::error::PRERELEASE must be true or false, got '$PRERELEASE'" >&2; exit 1 ;;
esac

# the globs are split without being expanded, so that a glob matching nothing is noticed
set -f
read -r -d '' -a patterns <<< "$FILES" || true
set +f
assets=()
shopt -s nullglob
for pattern in "${patterns[@]}"; do
# shellcheck disable=SC2206 # the glob is expanded on purpose
matches=($pattern)
if [ "${#matches[@]}" -eq 0 ]; then
echo "::error::No file matches '$pattern'" >&2
exit 1
fi
assets+=("${matches[@]}")
done
shopt -u nullglob

release_exists=false
if "$GH" release view "$VERSION" --repo "$REPOSITORY" --json tagName >/dev/null 2>&1; then
release_exists=true
fi
tag_exists=false
if "$GH" api "repos/$REPOSITORY/git/ref/tags/$VERSION" >/dev/null 2>&1; then
tag_exists=true
fi

if [ "$release_exists" = true ] || [ "$tag_exists" = true ]; then
if [ "$PRERELEASE" != true ]; then
echo "::error::$VERSION already exists: a release is not replaced" >&2
exit 1
fi
echo "Replacing the pre-release $VERSION"
if [ "$release_exists" = true ]; then
"$GH" release delete "$VERSION" --repo "$REPOSITORY" --yes
fi
if [ "$tag_exists" = true ]; then
"$GH" api -X DELETE "repos/$REPOSITORY/git/refs/tags/$VERSION" >/dev/null
fi
fi

args=(release create "$VERSION" --repo "$REPOSITORY" --target "$TARGET" --title "$TITLE" --generate-notes)
if [ "$PRERELEASE" = true ]; then
args+=(--prerelease)
fi
"$GH" "${args[@]}" "${assets[@]}"
echo "Published $VERSION on $TARGET${assets:+ with ${#assets[@]} file(s)}"
Loading
Loading