Skip to content

Commit 66c7d6b

Browse files
authored
Add initial forked implementation of versioning (#4)
Use a forked implementation of versioning; to be ported over to Golang later.
1 parent 58d3ce5 commit 66c7d6b

2 files changed

Lines changed: 399 additions & 0 deletions

File tree

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
name: Release
2+
on:
3+
workflow_call:
4+
inputs:
5+
branches:
6+
required: false
7+
type: string
8+
default: ${{ format('["{0}"]', github.event.repository.default_branch) }}
9+
10+
jobs:
11+
release-check:
12+
name: Check
13+
runs-on: ubuntu-latest
14+
steps:
15+
- run: echo "EOF=EOF$RANDOM" >> $GITHUB_ENV
16+
- id: pr
17+
name: Retrieve PR information
18+
env:
19+
PULL_REQUEST: ${{ toJSON(github.event.pull_request) }}
20+
uses: actions/github-script@v8
21+
with:
22+
script: |
23+
let pr = JSON.parse(process.env.PULL_REQUEST);
24+
if (pr == null) {
25+
const {data: pulls} = await github.rest.repos.listPullRequestsAssociatedWithCommit({
26+
owner: context.repo.owner,
27+
repo: context.repo.repo,
28+
commit_sha: context.sha
29+
});
30+
const open = pulls.filter(p => p.state == 'open');
31+
if (open.length == 0) {
32+
core.setFailed(`No open PR found for commit ${context.sha}`);
33+
return;
34+
}
35+
if (open.length > 1) {
36+
core.setFailed(`Multiple open PRs found for commit ${context.sha}`);
37+
return;
38+
}
39+
pr = open[0];
40+
}
41+
core.setOutput('json', JSON.stringify(pr));
42+
- uses: actions/checkout@v4
43+
- uses: actions/setup-go@v5
44+
with:
45+
go-version: stable
46+
- id: version
47+
name: Determine version
48+
env:
49+
GITHUB_TOKEN: ${{ github.token }}
50+
HEAD_FULL_NAME: ${{ fromJSON(steps.pr.outputs.json).head.repo.full_name }}
51+
HEAD_SHA: ${{ fromJSON(steps.pr.outputs.json).head.sha }}
52+
run: |
53+
# If `version.json` file doesn't exists, `version` is `""` and `404` is printed on stderr.
54+
# The step won't be marked as a failure though because the error happens in a subshell.
55+
version="$(gh api -X GET "repos/$HEAD_FULL_NAME/contents/version.json" -f ref="$HEAD_SHA" --jq '.content' | base64 -d | jq -r '.version')"
56+
echo "version=$version"
57+
echo "version=$version" >> $GITHUB_OUTPUT
58+
- id: branch
59+
name: Check if the branch is a release branch
60+
if: steps.version.outputs.version != ''
61+
env:
62+
BRANCHES: ${{ inputs.branches }}
63+
BASE_REF: ${{ fromJSON(steps.pr.outputs.json).base.ref }}
64+
uses: actions/github-script@v7
65+
with:
66+
script: |
67+
const branches = JSON.parse(process.env.BRANCHES);
68+
const release = branches.some(b => {
69+
const regexPattern = b.replace(/\*/g, '.*');
70+
const regex = new RegExp(`^${regexPattern}$`);
71+
return regex.test(process.env.BASE_REF);
72+
});
73+
console.log(`This is a release branch: ${release}`);
74+
core.setOutput('release', release);
75+
- id: tag
76+
name: Check if the tag already exists
77+
if: steps.version.outputs.version != ''
78+
env:
79+
VERSION: ${{ steps.version.outputs.version }}
80+
ALLOWED: ${{ steps.branch.outputs.release == 'true' || contains(fromJSON(steps.pr.outputs.json).labels.*.name, 'release') }}
81+
run: |
82+
git fetch origin --tags
83+
status=0
84+
git rev-list "$VERSION" &> /dev/null || status=$?
85+
if [[ $status == 0 ]]; then
86+
echo "exists=true" >> $GITHUB_OUTPUT
87+
echo "needed=false" >> $GITHUB_OUTPUT
88+
else
89+
echo "exists=false" >> $GITHUB_OUTPUT
90+
echo "needed=$ALLOWED" >> $GITHUB_OUTPUT
91+
fi
92+
- name: Install semver (node command line tool)
93+
if: steps.tag.outputs.needed == 'true'
94+
run: npm install -g "https://github.com/npm/node-semver#dc0fe202faaf19a545ce5eeab3480be84180a082" # v7.3.8
95+
- name: Check version
96+
if: steps.tag.outputs.needed == 'true'
97+
env:
98+
VERSION: ${{ steps.version.outputs.version }}
99+
# semver fails if the version is not valid (e.g. v0.1 would fail)
100+
run: semver "$VERSION"
101+
- id: prerelease
102+
if: steps.tag.outputs.needed == 'true'
103+
name: Check if this is a pre-release
104+
env:
105+
VERSION: ${{ steps.version.outputs.version }}
106+
# semver -r fails if the version is not valid or if it is a pre-release (e.g v0.1 or v0.1.0-rc1 would fail)
107+
run: echo "prerelease=$(semver -r "$VERSION" && echo false || echo true)" >> $GITHUB_OUTPUT
108+
- id: prev
109+
name: Determine version number to compare to
110+
if: steps.tag.outputs.needed == 'true'
111+
env:
112+
VERSION: ${{ steps.version.outputs.version }}
113+
PRERELEASE: ${{ steps.prerelease.outputs.prerelease }}
114+
# We need to determine the version number we want to compare to,
115+
# taking into account that this might be a (patch) release on a release branch.
116+
# Example:
117+
# Imagine a module that has releases for v0.1.0, v0.2.0, v0.3.0 and v0.3.0-rc1.
118+
# When trying to cut a release v0.2.1, we need to base our comparisons on v0.2.0.
119+
# When trying to cut a release v0.3.1 or v0.4.0, we need to base our comparisons on v0.3.0.
120+
# When trying to cut a release v0.3.0-rc2, we need to base our comparisons on v0.3.0-rc1.
121+
run: |
122+
git fetch origin --tags
123+
go install github.com/marten-seemann/semver-highest@fcdc98f8820ff0e6613c1bee071c096febd98dbf
124+
vs=$(git tag | paste -sd , -)
125+
if [[ ! -z "$vs" ]]; then
126+
v=$(semver-highest -target "$VERSION" -versions "$vs" -prerelease="$PRERELEASE")
127+
status=$?
128+
if [[ $status != 0 ]]; then
129+
echo $v
130+
exit $status
131+
fi
132+
echo "version=$v" >> $GITHUB_OUTPUT
133+
fi
134+
- id: git-diff
135+
name: run git diff on go.mod file(s)
136+
if: steps.tag.outputs.needed == 'true' && steps.prev.outputs.version != ''
137+
env:
138+
PREV_VERSION: ${{ steps.prev.outputs.version }}
139+
run: |
140+
# First get the diff for the go.mod file in the root directory...
141+
output=$(git diff "$PREV_VERSION..HEAD" -- './go.mod')
142+
# ... then get the diff for all go.mod files in subdirectories.
143+
# Note that this command also finds go.mod files more than one level deep in the directory structure.
144+
output+=$(git diff "$PREV_VERSION..HEAD" -- '*/go.mod')
145+
if [[ -z "$output" ]]; then
146+
output="(empty)"
147+
fi
148+
printf "output<<$EOF\n%s\n$EOF" "$output" >> $GITHUB_OUTPUT
149+
- id: gorelease
150+
name: Run gorelease
151+
if: steps.tag.outputs.needed == 'true' && steps.prev.outputs.version != ''
152+
env:
153+
PREV_VERSION: ${{ steps.prev.outputs.version }}
154+
# see https://github.com/golang/exp/commits/master/cmd/gorelease
155+
run: |
156+
go mod download
157+
go install golang.org/x/exp/cmd/gorelease@f062dba9d201f5ec084d25785efec05637818c00 # https://cs.opensource.google/go/x/exp/+/f062dba9d201f5ec084d25785efec05637818c00
158+
output=$((gorelease -base "$PREV_VERSION") 2>&1 || true)
159+
printf "output<<$EOF\n%s\n$EOF" "$output" >> $GITHUB_OUTPUT
160+
- id: gocompat
161+
name: Check Compatibility
162+
if: steps.tag.outputs.needed == 'true' && steps.prev.outputs.version != ''
163+
env:
164+
PREV_VERSION: ${{ steps.prev.outputs.version }}
165+
run: |
166+
go install github.com/smola/gocompat/cmd/gocompat@8498b97a44792a3a6063c47014726baa63e2e669 # v0.3.0
167+
output=$(gocompat compare --go1compat --git-refs="$PREV_VERSION..HEAD" ./... || true)
168+
if [[ -z "$output" ]]; then
169+
output="(empty)"
170+
fi
171+
printf "output<<$EOF\n%s\n$EOF" "$output" >> $GITHUB_OUTPUT
172+
- id: release
173+
if: steps.tag.outputs.needed == 'true' && fromJSON(steps.pr.outputs.json).head.repo.full_name == fromJSON(steps.pr.outputs.json).base.repo.full_name
174+
uses: galargh/action-gh-release@571276229e7c9e6ea18f99bad24122a4c3ec813f # https://github.com/galargh/action-gh-release/pull/1
175+
with:
176+
draft: true
177+
tag_name: ${{ steps.version.outputs.version }}
178+
generate_release_notes: true
179+
target_commitish: ${{ fromJSON(steps.pr.outputs.json).base.ref }}
180+
- id: message
181+
if: steps.tag.outputs.exists == 'false'
182+
env:
183+
HEADER: |
184+
Suggested version: `${{ steps.version.outputs.version }}`
185+
BODY: |
186+
Comparing to: [${{ steps.prev.outputs.version }}](${{ fromJSON(steps.pr.outputs.json).base.repo.html_url }}/releases/tag/${{ steps.prev.outputs.version }}) ([diff](${{ fromJSON(steps.pr.outputs.json).base.repo.html_url }}/compare/${{ steps.prev.outputs.version }}..${{ fromJSON(steps.pr.outputs.json).head.label }}))
187+
188+
Changes in `go.mod` file(s):
189+
```diff
190+
${{ steps.git-diff.outputs.output }}
191+
```
192+
193+
`gorelease` says:
194+
```
195+
${{ steps.gorelease.outputs.output }}
196+
```
197+
198+
`gocompat` says:
199+
```
200+
${{ steps.gocompat.outputs.output }}
201+
```
202+
203+
BODY_ALT: |
204+
This is the first release of this module.
205+
206+
RELEASE_BRANCH_NOTICE: |
207+
## Cutting a Release (when not on `${{ inputs.branches }}`)
208+
209+
This PR is targeting `${{ fromJSON(steps.pr.outputs.json).base.ref }}`, which is not any of ${{ inputs.branches }}.
210+
If you wish to cut a release once this PR is merged, please add the `release` label to this PR.
211+
212+
DIFF_NOTICE: |
213+
## Cutting a Release (and modifying non-markdown files)
214+
215+
This PR is modifying both `version.json` and non-markdown files.
216+
The Release Checker is not able to analyse files that are not checked in to `${{ fromJSON(steps.pr.outputs.json).base.ref }}`. This might cause the above analysis to be inaccurate.
217+
Please consider performing all the code changes in a separate PR before cutting the release.
218+
219+
RELEASE_NOTICE: |
220+
## Automatically created GitHub Release
221+
222+
A [draft GitHub Release](${{ steps.release.outputs.url }}) has been created.
223+
It is going to be published when this PR is merged.
224+
You can modify its' body to include any release notes you wish to include with the release.
225+
226+
RELEASE_NOTICE_ALT: |
227+
## Automatically created GitHub Release
228+
229+
Pre-creating GitHub Releases on release PRs initiated from forks is not supported.
230+
If you wish to prepare release notes yourself, you should create a draft GitHub Release for tag `${{ steps.version.outputs.version }}` manually.
231+
The draft GitHub Release is going to be published when this PR is merged.
232+
If you choose not to create a draft GitHub Release, a published GitHub Released is going to be created when this PR is merged.
233+
234+
BASE_REF: ${{ fromJSON(steps.pr.outputs.json).base.ref }}
235+
HEAD_LABEL: ${{ fromJSON(steps.pr.outputs.json).head.label }}
236+
HEAD_FULL_NAME: ${{ fromJSON(steps.pr.outputs.json).head.repo.full_name }}
237+
GITHUB_TOKEN: ${{ github.token }}
238+
PREV_VERSION: ${{ steps.prev.outputs.version }}
239+
TAG_NEEDED: ${{ steps.tag.outputs.needed }}
240+
run: |
241+
echo "output<<$EOF" >> $GITHUB_OUTPUT
242+
243+
if [[ "$TAG_NEEDED" == "false" ]]; then
244+
echo "$RELEASE_BRANCH_NOTICE" >> $GITHUB_OUTPUT
245+
else
246+
echo "$HEADER" >> $GITHUB_OUTPUT
247+
if [[ "$PREV_VERSION" != "" ]]; then
248+
echo "$BODY" >> $GITHUB_OUTPUT
249+
else
250+
echo "$BODY_ALT" >> $GITHUB_OUTPUT
251+
fi
252+
diff="$(gh api -X GET "repos/$GITHUB_REPOSITORY/compare/$BASE_REF...$HEAD_LABEL" --jq '.files | map(.filename) | map(select(test("^(version\\.json|.*\\.md)$") | not)) | .[]')"
253+
if [[ "$diff" != "" ]]; then
254+
echo "$DIFF_NOTICE" >> $GITHUB_OUTPUT
255+
fi
256+
if [[ "$GITHUB_REPOSITORY" == "$HEAD_FULL_NAME" ]]; then
257+
echo "$RELEASE_NOTICE" >> $GITHUB_OUTPUT
258+
else
259+
echo "$RELEASE_NOTICE_ALT" >> $GITHUB_OUTPUT
260+
fi
261+
fi
262+
echo "$EOF" >> $GITHUB_OUTPUT
263+
- name: Post message on PR
264+
uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # v2.9.0
265+
if: steps.tag.outputs.exists == 'false'
266+
with:
267+
header: release-check
268+
recreate: true
269+
message: ${{ steps.message.outputs.output }}
270+
number: ${{ fromJSON(steps.pr.outputs.json).number }}

0 commit comments

Comments
 (0)