Repository navigation
Expand file tree
/
Copy pathaction.yml
More file actions
261 lines (229 loc) · 10.9 KB
/
Copy pathaction.yml
File metadata and controls
261 lines (229 loc) · 10.9 KB
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
name: 'FerrFlow Release'
description: 'Universal semantic versioning for monorepos and classic repos'
author: 'FerrLabs'
branding:
icon: 'tag'
color: 'orange'
inputs:
version:
description: 'FerrFlow version to use (e.g. 1.2.3). Defaults to latest.'
required: false
default: 'latest'
mode:
description: 'Action mode: "release" runs the full release pipeline. "preview" posts a PR comment with version bump preview. "publish" runs the configured publishers for the currently-released version (no bump/tag) — for a separate job that has the build toolchain + registry auth the publishers need.'
required: false
default: 'release'
dry_run:
description: 'Run without creating releases or pushing changes'
required: false
default: 'false'
force_version:
description: 'Force a specific version, skipping commit analysis. Format: VERSION (single repo) or NAME@VERSION (monorepo)'
required: false
default: ''
package:
description: 'For mode "publish": space-separated package names to publish (e.g. "api web"). Omit to auto-detect from the triggering tag (GITHUB_REF), falling back to every package.'
required: false
default: ''
bot:
description: 'Opt into the hosted FerrFlow bot identity (ferrflow[bot]). Requires permissions.id-token: write on the caller workflow. If false/unset, the caller''s token or GITHUB_TOKEN is used as before.'
required: false
default: 'false'
bot_endpoint:
description: 'Override the hosted bot token endpoint. Must be https, and any host other than api.ferrflow.com also needs FERRFLOW_BOT_ALLOW_CUSTOM_ENDPOINT=1 in the job env, since the value decides where the runner''s OIDC identity token is sent. Defaults to https://api.ferrflow.com/ferrflow/token.'
required: false
default: 'https://api.ferrflow.com/ferrflow/token'
bot_audience:
description: 'OIDC audience requested from the GitHub Actions runner. Must match the server-side expected audience.'
required: false
default: 'ferrflow.ferrlabs.com'
tag_signing_key:
description: 'SSH private key that signs the release tag. GitHub verifies a tag against the keys of the account whose verified email the tagger carries, so the matching public key must be registered as a signing key on that account, and tag_signing_email must be one of its verified emails. A GitHub App identity such as ferrflow[bot] cannot be used: apps hold no keys, and GitHub signs the commits it creates through the API but not tag objects.'
required: false
default: ''
tag_signing_name:
description: 'Tagger name used when tag_signing_key is set.'
required: false
default: ''
tag_signing_email:
description: 'Tagger email used when tag_signing_key is set. Must be a verified email on the account that owns the signing key.'
required: false
default: ''
outputs:
released:
description: '"true" when mode "release" tagged at least one package, otherwise "false". Always "false" on a dry run.'
value: ${{ steps.ferrflow.outputs.released }}
releases:
description: 'JSON array of the packages tagged by mode "release", each with name, version and tag. "[]" when nothing was released.'
value: ${{ steps.ferrflow.outputs.releases }}
runs:
using: composite
steps:
- name: Install FerrFlow
shell: bash
env:
INPUT_VERSION: ${{ inputs.version }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [ "$INPUT_VERSION" != "latest" ] && ! printf '%s' "$INPUT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9._-]+)*$'; then
echo "Invalid version: '$INPUT_VERSION' (expected 'latest' or X.Y.Z)" >&2
exit 1
fi
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
# Git Bash / MSYS / Cygwin on Windows runners all report a compound
# kernel name like "mingw64_nt-10.0". Normalise to "windows".
case "$OS" in
linux) PLATFORM="linux" ; EXT="tar.gz" ;;
darwin) PLATFORM="darwin" ; EXT="tar.gz" ;;
mingw*|msys*|cygwin*) PLATFORM="windows" ; EXT="zip" ;;
*) echo "Unsupported OS: $OS" && exit 1 ;;
esac
case "$ARCH" in
x86_64) ARCH_NAME="x64" ;;
aarch64|arm64) ARCH_NAME="arm64" ;;
armv7l|armv7) ARCH_NAME="armv7" ;;
*) echo "Unsupported architecture: $ARCH" && exit 1 ;;
esac
ARCHIVE="ferrflow-${PLATFORM}-${ARCH_NAME}.${EXT}"
if [ "$INPUT_VERSION" = "latest" ]; then
BASE="https://github.com/FerrLabs/FerrFlow/releases/latest/download"
else
BASE="https://github.com/FerrLabs/FerrFlow/releases/download/v${INPUT_VERSION}"
fi
INSTALL_DIR="${RUNNER_TEMP:-$HOME/.local/bin}/ferrflow-bin"
DOWNLOAD_DIR="$(mktemp -d)"
mkdir -p "$INSTALL_DIR"
curl --fail --location --silent --show-error \
--output "${DOWNLOAD_DIR}/${ARCHIVE}" "${BASE}/${ARCHIVE}"
# Verify the digest before anything is extracted or executed. A
# release without SHA256SUMS predates #789; warn rather than break
# every consumer pinned to an older version.
if curl --fail --location --silent --show-error \
--output "${DOWNLOAD_DIR}/SHA256SUMS" "${BASE}/SHA256SUMS"; then
grep " \*\?${ARCHIVE}\$" "${DOWNLOAD_DIR}/SHA256SUMS" > "${DOWNLOAD_DIR}/expected.sums" || true
if [ ! -s "${DOWNLOAD_DIR}/expected.sums" ]; then
echo "SHA256SUMS has no entry for ${ARCHIVE} — refusing to install" >&2
exit 1
fi
( cd "$DOWNLOAD_DIR" && sha256sum -c expected.sums )
echo "Verified ${ARCHIVE} against SHA256SUMS"
else
echo "::warning::No SHA256SUMS published for this release; skipping checksum verification. Pin a version >= 5.52.0 to get verified installs."
fi
# Provenance is bound to a specific release, so it can only be
# checked on a pinned version. gh is preinstalled on GitHub-hosted
# runners; skip rather than fail where it isn't available.
if [ "$INPUT_VERSION" != "latest" ] && command -v gh >/dev/null 2>&1 && [ -n "${GITHUB_TOKEN:-}" ]; then
if gh attestation verify "${DOWNLOAD_DIR}/${ARCHIVE}" --repo FerrLabs/FerrFlow >/dev/null 2>&1; then
echo "Verified build provenance for ${ARCHIVE}"
else
echo "::warning::Could not verify build provenance for ${ARCHIVE} (older release, or gh attestation unavailable)"
fi
fi
if [ "$PLATFORM" = "windows" ]; then
unzip -q -o "${DOWNLOAD_DIR}/${ARCHIVE}" -d "$INSTALL_DIR"
# No chmod needed on Windows; the .exe is already executable.
else
tar -xzf "${DOWNLOAD_DIR}/${ARCHIVE}" -C "$INSTALL_DIR"
chmod +x "$INSTALL_DIR/ferrflow"
fi
rm -rf "$DOWNLOAD_DIR"
echo "$INSTALL_DIR" >> "$GITHUB_PATH"
# Git identity (`user.name` / `user.email`) is set by the binary
# itself: `ensure_bot_token` in bot mode, otherwise
# `ensure_commit_identity` falls back to github-actions[bot]. Self-hosters
# running their own App can override via `FERRFLOW_BOT_LOGIN` and
# `FERRFLOW_BOT_USER_ID` env vars on the calling job.
- name: Configure tag signing
if: inputs.tag_signing_key != '' && inputs.mode == 'release'
shell: bash
env:
TAG_SIGNING_KEY: ${{ inputs.tag_signing_key }}
TAG_SIGNING_NAME: ${{ inputs.tag_signing_name }}
TAG_SIGNING_EMAIL: ${{ inputs.tag_signing_email }}
run: |
set -euo pipefail
if [ -z "$TAG_SIGNING_NAME" ] || [ -z "$TAG_SIGNING_EMAIL" ]; then
echo "::error::tag_signing_key needs tag_signing_name and tag_signing_email. GitHub verifies a tag against the keys of the account whose verified email the tagger carries, so an unnamed tagger cannot be verified."
exit 1
fi
key="${RUNNER_TEMP}/ferrflow-tag-signing-key"
umask 077
printf '%s\n' "$TAG_SIGNING_KEY" > "$key"
if ! ssh-keygen -y -f "$key" > "${key}.pub" 2>/dev/null; then
echo "::error::tag_signing_key is not a usable OpenSSH private key. It must be unencrypted (ssh-keygen -t ed25519 -N '')."
exit 1
fi
git config gpg.format ssh
git config user.signingkey "$key"
git config tag.gpgSign true
{
echo "GIT_COMMITTER_NAME=${TAG_SIGNING_NAME}"
echo "GIT_COMMITTER_EMAIL=${TAG_SIGNING_EMAIL}"
} >> "$GITHUB_ENV"
- name: Optimize commit-graph
if: inputs.mode != 'publish'
shell: bash
run: git commit-graph write --reachable 2>/dev/null || true
- name: Preview release
if: inputs.mode == 'preview'
shell: bash
env:
FERRFLOW_BOT: ${{ inputs.bot }}
FERRFLOW_BOT_ENDPOINT: ${{ inputs.bot_endpoint }}
FERRFLOW_BOT_AUDIENCE: ${{ inputs.bot_audience }}
run: ferrflow check --comment
- name: Run FerrFlow release
id: ferrflow
if: inputs.mode == 'release'
shell: bash
env:
FERRFLOW_BOT: ${{ inputs.bot }}
FERRFLOW_BOT_ENDPOINT: ${{ inputs.bot_endpoint }}
FERRFLOW_BOT_AUDIENCE: ${{ inputs.bot_audience }}
INPUT_DRY_RUN: ${{ inputs.dry_run }}
INPUT_FORCE_VERSION: ${{ inputs.force_version }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
args=()
[ "$INPUT_DRY_RUN" = "true" ] && args+=(--dry-run)
args+=(release)
if [ -n "$INPUT_FORCE_VERSION" ]; then
if ! printf '%s' "$INPUT_FORCE_VERSION" | grep -Eq '^([A-Za-z0-9._@/-]+@)?[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9._-]+)*$'; then
echo "Invalid force_version: '$INPUT_FORCE_VERSION' (expected VERSION or NAME@VERSION)" >&2
exit 1
fi
args+=(--force-version "$INPUT_FORCE_VERSION")
fi
if [[ "$(ferrflow release --help)" == *--output-format* ]]; then
args+=(--output-format github)
else
echo "::warning::FerrFlow ${INPUT_VERSION} cannot report release outputs; the released and releases outputs stay empty."
fi
ferrflow "${args[@]}"
- name: Run FerrFlow publishers
if: inputs.mode == 'publish'
shell: bash
env:
FERRFLOW_BOT: ${{ inputs.bot }}
FERRFLOW_BOT_ENDPOINT: ${{ inputs.bot_endpoint }}
FERRFLOW_BOT_AUDIENCE: ${{ inputs.bot_audience }}
INPUT_DRY_RUN: ${{ inputs.dry_run }}
INPUT_PACKAGE: ${{ inputs.package }}
run: |
set -euo pipefail
args=()
[ "$INPUT_DRY_RUN" = "true" ] && args+=(--dry-run)
args+=(publish)
read -ra packages <<< "$INPUT_PACKAGE"
for pkg in ${packages[@]+"${packages[@]}"}; do
if ! printf '%s' "$pkg" | grep -Eq '^[A-Za-z0-9._@/-]+$'; then
echo "Invalid package name: '$pkg'" >&2
exit 1
fi
args+=("$pkg")
done
ferrflow "${args[@]}"