-
Notifications
You must be signed in to change notification settings - Fork 0
158 lines (154 loc) Β· 7.31 KB
/
Copy pathgithub-release.yml
File metadata and controls
158 lines (154 loc) Β· 7.31 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
name: π GitHub release
# PRESET β seeded once from template/presets/ (onboarding or first cascade),
# then repo-owned: customize what THIS repo releases. The fleet base actions
# do the heavy lifting β github-release-app-token mints the release App token,
# github-release cuts the immutable 3-step release tied to the pushed tag
# (create --draft β upload assets β edit --draft=false).
#
# Default flow: push a signed v* tag β this workflow cuts a GitHub Release for
# it. A manual dispatch is a DRY-RUN (prints the cut plan) unless
# `release: true`. Add build steps + assets for whatever this repo ships.
#
# ORDER RULE: the tag + immutable GH release are the FINAL markers of a
# release β they may only exist AFTER the registry publish is live. A STAGED
# npm package is not published (staging may never be approved), so this
# workflow refuses to cut when the tagged version is not resolvable on its
# registry (npm packument / crates.io sparse index). Registry-less repos
# (private, github-release-only) skip the gate.
on:
workflow_dispatch:
inputs:
release:
description: 'Cut the release for real (false = dry-run plan, the default).'
type: boolean
default: false
tag:
description: 'Tag to release. Required on dispatch (tag pushes use the pushed tag).'
type: string
default: ''
push:
tags:
- 'v*'
permissions:
contents: read
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
# First step must be the third-party actions/checkout (GitHub fetches it
# independently) to populate the workspace so the LOCAL
# ./.github/actions/* composite resolves; the fleet checkout action then
# re-checks-out at its own depth.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-15)
with:
fetch-depth: 1
persist-credentials: false
- name: Checkout
uses: ./.github/actions/fleet/checkout
- name: Resolve tag
id: tag
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_TAG: ${{ inputs.tag }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ "${EVENT_NAME}" = "push" ]; then
TAG="${REF_NAME}"
elif [ -n "${INPUT_TAG}" ]; then
TAG="${INPUT_TAG}"
else
echo "Γ a manual dispatch needs the tag input (tag pushes resolve it from the ref)." >&2
echo " Fix: re-dispatch with tag: v<version> (the tag must already be pushed)." >&2
exit 1
fi
echo "tag=${TAG}" >> "${GITHUB_OUTPUT}"
# Belt-and-braces publish-before-release gate: refuse to cut when the
# tagged version is not actually live on its registry. Runs only when a
# real cut would happen (tag push, or dispatch with release: true) so a
# dry-run plan stays inspectable pre-publish. curl (not npm view) β the
# runner has no install here and npm trips repos' pnpm devEngines.
- name: Registry publish is live
if: ${{ github.event_name == 'push' || inputs.release }}
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
VERSION="${TAG#v}"
if [ -f package.json ] && [ "$(node -p "require('./package.json').private === true")" != "true" ]; then
NAME="$(node -p "require('./package.json').name")"
if ! curl -fsS -o /dev/null "https://registry.npmjs.org/${NAME}/${VERSION}"; then
echo "Γ ${NAME}@${VERSION} is not resolvable on npm β refusing to cut the GH release before the registry publish." >&2
echo " A STAGED package is not published. Fix: approve/complete the publish first, then re-run." >&2
exit 1
fi
echo "β ${NAME}@${VERSION} is live on npm."
elif [ -f Cargo.toml ]; then
# Single crate: the root [package] name. Workspace: every
# publishable member's name (publish = false members are skipped).
# Empty output + exit 0 = nothing publishable (stub-only
# workspace); a malformed manifest fails LOUD instead of dying
# silently under `set -e`.
NAMES="$(node -e '
const fs = require("node:fs")
const root = fs.readFileSync("Cargo.toml", "utf8")
const pkgName = /^\[package\][^]*?^name *= *"([^"]+)"/m.exec(root)
if (pkgName) {
console.log(pkgName[1])
process.exit(0)
}
const members = /^members *= *\[([^\]]*)\]/m.exec(root)
if (!members) {
console.error("Γ Cargo.toml has neither a [package] name nor a [workspace] members list β cannot derive crate names for the registry-liveness gate.")
console.error(" Fix: give the root manifest a [package] section or a members = [...] list.")
process.exit(1)
}
const entries = [...members[1].matchAll(/"([^"]+)"/g)].map(m => m[1])
const dirs = entries.flatMap(e => e.includes("*") ? fs.globSync(e) : [e])
const names = []
for (const dir of dirs) {
const manifestPath = `${dir}/Cargo.toml`
if (!fs.existsSync(manifestPath)) { continue }
const manifest = fs.readFileSync(manifestPath, "utf8")
if (/^publish *= *false/m.test(manifest)) { continue }
const name = /^name *= *"([^"]+)"/m.exec(manifest)
if (name) { names.push(name[1]) }
}
console.log(names.join("\n"))
')"
if [ -z "${NAMES}" ]; then
echo "No publishable crate in the workspace β skipping the crates.io liveness gate."
fi
for NAME in ${NAMES}; do
case "${#NAME}" in
1) IDX="1/${NAME}" ;;
2) IDX="2/${NAME}" ;;
3) IDX="3/${NAME:0:1}/${NAME}" ;;
*) IDX="${NAME:0:2}/${NAME:2:2}/${NAME}" ;;
esac
if ! curl -fsS "https://index.crates.io/${IDX}" | grep -q "\"vers\":\"${VERSION}\""; then
echo "Γ ${NAME}@${VERSION} is not in the crates.io index β refusing to cut the GH release before the registry publish." >&2
exit 1
fi
echo "β ${NAME}@${VERSION} is live on crates.io."
done
else
echo "No public npm package or crate manifest β skipping the registry-liveness gate (github-release-only repo)."
fi
- name: Mint release-app token
id: app-token
uses: ./.github/actions/fleet/github-release-app-token
with:
client-id: ${{ vars.SOCKET_RELEASE_CLIENT_ID }}
private-key: ${{ secrets.SOCKET_RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
# Build this repo's release assets here, then list them under the cut
# step's `assets:` (newline-separated paths; each must exist).
- name: Cut immutable GitHub release
uses: ./.github/actions/fleet/github-release
with:
tag: ${{ steps.tag.outputs.tag }}
dry-run: ${{ (github.event_name == 'push' || inputs.release) && 'false' || 'true' }}
token: ${{ steps.app-token.outputs.token }}