1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
name: Release
on:
push:
branches:
permissions:
contents: read
# One release at a time; do not cancel one mid-flight.
concurrency:
group: release
cancel-in-progress: false
env:
# Derive the image name from the repository so it tracks a rename automatically
# instead of drifting (the hardcoded name once lagged a rename and published under
# the old name). `github.repository` is `owner/repo`, lowercase as GHCR requires.
IMAGE: ghcr.io/${{ github.repository }}
# OCI namespace for the Helm chart, shared across this owner's charts. The chart is
# pushed as `<owner>/charts/git-cache-proxy`.
CHART_REPO: oci://ghcr.io/${{ github.repository_owner }}/charts
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
released: ${{ steps.release.outputs.released }}
version: ${{ steps.release.outputs.version }}
steps:
# The version-bump commit is pushed directly to `main`, which is protected by a
# branch ruleset that requires status checks. The built-in GITHUB_TOKEN cannot be
# granted a ruleset bypass, so mint a token for a GitHub App that is on the
# ruleset's bypass list and push with that instead.
- name: Mint a token for the release app
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- uses: actions/checkout@v7
with:
# Full history and tags so knope can read the commits since the last release.
fetch-depth: 0
# Push the release commit as the app so it bypasses the branch ruleset.
token: ${{ steps.app-token.outputs.token }}
- uses: knope-dev/action@v2.1.2
with:
version: 0.23.0
- name: Configure the release identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# knope bumps the version, updates the changelog, tags, and creates the GitHub
# release from the conventional commits since the last tag. The push uses the app
# token, so - unlike the built-in token - it retriggers this workflow; that rerun
# finds no releasable commits and no-ops via `no_release`, so the publish job runs
# only in the run that actually released. `no_release` is a normal no-op, not a
# failure.
- name: Release
id: release
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set +e
out="$(knope release 2>&1)"
code=$?
echo "$out"
if [ "$code" -eq 0 ]; then
# Read the version knope just bumped in the working tree. Do NOT derive it
# from `git tag`: with [github] configured, knope creates the tag via the
# GitHub API, so it never appears as a local tag in this checkout, and
# `git tag | head` would return the *previous* release instead.
version="$(grep -m1 '^version = ' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')"
echo "released=true" >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "::notice::Released v${version}."
elif echo "$out" | grep -q 'no_release'; then
echo "::notice::No releasable commits since the last release; nothing to do."
echo "released=false" >> "$GITHUB_OUTPUT"
else
exit "$code"
fi
# Build and push the versioned image to GHCR, in the same run that cut the release, at
# the tag knope just created. The published GHCR package starts private; make it public
# once to allow anonymous `docker pull`.
publish:
name: Publish image
needs: release
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7
with:
ref: v${{ needs.release.outputs.version }}
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push the versioned image
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile
push: true
tags: |
${{ env.IMAGE }}:${{ needs.release.outputs.version }}
${{ env.IMAGE }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
# Publish the crate to crates.io (which feeds docs.rs) in the same run that cut the
# release, at the tag knope just created. Uses crates.io trusted publishing: GitHub's
# OIDC token is exchanged for a short-lived crates.io token, so no API token secret is
# stored. One-time bootstrap before this can work: publish once manually to claim the
# name (`cargo publish`), then add a GitHub Actions trusted publisher on the crate's
# crates.io settings for this repo and the Release workflow.
publish-crate:
name: Publish crate
needs: release
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
with:
ref: v${{ needs.release.outputs.version }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Authenticate to crates.io
id: auth
uses: rust-lang/crates-io-auth-action@v1
- name: Publish to crates.io
run: cargo publish --locked
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
# Package and push the Helm chart to GHCR as an OCI artifact, in the same run that
# cut the release, at the tag knope just created. The published GHCR package starts
# private; make it public once to allow anonymous `helm pull`.
publish-chart:
name: Publish chart
needs: release
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7
with:
ref: v${{ needs.release.outputs.version }}
- uses: azure/setup-helm@v5
# Version the chart in lockstep with the app: package at the released version
# (overriding the static Chart.yaml version) with the matching appVersion, so a
# pulled chart deploys the image it was cut with.
- name: Package and push the chart
run: |
helm package chart -d dist \
--version "${{ needs.release.outputs.version }}" \
--app-version "${{ needs.release.outputs.version }}"
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u "${{ github.actor }}" --password-stdin
helm push dist/git-cache-proxy-*.tgz "${{ env.CHART_REPO }}"