@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
yml 260 lines 11 KB
Raw
1 name: Release Proposal
2
3 # This workflow creates a release proposal as a PR that requires approval from maintainers
4 # Triggered manually by maintainers when ready to prepare a release
5 on:
6 workflow_dispatch:
7 inputs:
8 version:
9 description: 'Version to release (e.g., v2.8.0)'
10 required: true
11 type: string
12 commit_hash:
13 description: 'Commit hash to release from'
14 required: true
15 type: string
16
17 permissions:
18 contents: read
19
20 jobs:
21 create-proposal:
22 name: Create Release Proposal
23 runs-on: ubuntu-latest
24 permissions:
25 contents: write
26 pull-requests: write
27 issues: write
28
29 steps:
30 - name: Harden the runner (Audit all outbound calls)
31 uses: step-security/harden-runner@46b06cea07f9e64621fdf066ba77ac6915163d43 # v2.22.3
32 with:
33 egress-policy: audit
34 - name: Checkout code
35 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36 with:
37 fetch-depth: 0
38
39 # Inputs, step outputs and other untrusted text (commit subjects are
40 # contributor-written PR titles) are only ever passed through env:, never
41 # interpolated as expressions into run: or script: bodies.
42 - name: Trim and validate inputs
43 id: inputs
44 env:
45 INPUT_VERSION: ${{ inputs.version }}
46 INPUT_COMMIT_HASH: ${{ inputs.commit_hash }}
47 run: |
48 # Trim whitespace from inputs
49 VERSION=$(echo "$INPUT_VERSION" | xargs)
50 COMMIT_HASH=$(echo "$INPUT_COMMIT_HASH" | xargs)
51
52 # Validate version format
53 if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
54 echo "Error: Version must follow semver format (e.g., v2.8.0 or v2.8.0-beta.1)"
55 exit 1
56 fi
57
58 # Validate commit hash format
59 if [[ ! "$COMMIT_HASH" =~ ^[a-f0-9]{7,40}$ ]]; then
60 echo "Error: Commit hash must be a valid SHA (7-40 characters)"
61 exit 1
62 fi
63
64 # Resolve to the full SHA. This fails if the commit doesn't exist or
65 # a short SHA is ambiguous, and from here on only the full SHA is used.
66 if ! FULL_COMMIT_HASH=$(git rev-parse --verify --quiet "${COMMIT_HASH}^{commit}"); then
67 echo "Error: Commit $COMMIT_HASH does not exist or is ambiguous"
68 exit 1
69 fi
70
71 echo "version=$VERSION" >> "$GITHUB_OUTPUT"
72 echo "commit_hash=$FULL_COMMIT_HASH" >> "$GITHUB_OUTPUT"
73
74 - name: Check if tag already exists
75 env:
76 VERSION: ${{ steps.inputs.outputs.version }}
77 run: |
78 if git rev-parse "refs/tags/$VERSION" >/dev/null 2>&1; then
79 echo "Error: Tag $VERSION already exists"
80 exit 1
81 fi
82
83 - name: Check for existing proposal PR
84 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
85 env:
86 VERSION: ${{ steps.inputs.outputs.version }}
87 with:
88 script: |
89 const { findOpenProposals } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/release-proposal.js`);
90 const version = process.env.VERSION;
91
92 const [existing] = await findOpenProposals({ github, context, version });
93 if (existing) {
94 const labels = existing.labels.map(label => label.name);
95 if (labels.includes('released') || labels.includes('release-in-progress')) {
96 core.setFailed(`A release for ${version} is already in progress or completed: ${existing.html_url}`);
97 } else {
98 core.setFailed(`An open release proposal already exists for ${version}: ${existing.html_url}\n\nPlease use the existing PR or close it first.`);
99 }
100 return;
101 }
102 console.log(`No open proposal found for ${version}, proceeding...`);
103
104 - name: Generate changelog and create branch
105 id: setup
106 env:
107 GH_TOKEN: ${{ github.token }}
108 VERSION: ${{ steps.inputs.outputs.version }}
109 COMMIT_HASH: ${{ steps.inputs.outputs.commit_hash }}
110 REF_NAME: ${{ github.ref_name }}
111 run: |
112 # Create a new branch for the release proposal
113 BRANCH_NAME="release_proposal-$VERSION"
114 git checkout -b "$BRANCH_NAME"
115
116 # Describe where the commit is relative to the branch this workflow
117 # was run from. The commit may be on a maintenance branch (e.g. 2.11)
118 # instead, in which case "behind HEAD" would be meaningless.
119 if git merge-base --is-ancestor "$COMMIT_HASH" HEAD; then
120 COMMITS_BEHIND=$(git rev-list --count "${COMMIT_HASH}..HEAD")
121 if [ "$COMMITS_BEHIND" -eq 0 ]; then
122 BEHIND_INFO="This is the latest commit on $REF_NAME"
123 else
124 BEHIND_INFO="This commit is **${COMMITS_BEHIND} commits behind $REF_NAME**"
125 fi
126 else
127 COMMIT_BRANCHES=$(git branch -r --contains "$COMMIT_HASH" --format='%(refname:lstrip=3)' | grep -v '^HEAD$' | paste -sd, - | sed 's/,/, /g')
128 BEHIND_INFO="This commit is not on $REF_NAME; it is on: ${COMMIT_BRANCHES:-no branch}"
129 fi
130 echo "behind_info=$BEHIND_INFO" >> "$GITHUB_OUTPUT"
131
132 # Base the changelog on the previous *published* release by version,
133 # not the last tag, and leave out commits that already shipped in it
134 # as backports. See the scripts for details.
135 LAST_RELEASE=$(.github/scripts/previous-release.sh "$VERSION")
136
137 if [ -z "$LAST_RELEASE" ]; then
138 echo "No previous release found, generating full changelog"
139 COMMITS=$(git log --pretty=format:"- %s (%h)" --reverse "$COMMIT_HASH")
140 else
141 echo "Generating changelog since $LAST_RELEASE"
142 CHANGELOG_SHAS=$(.github/scripts/changelog-commits.sh "$LAST_RELEASE" "$COMMIT_HASH")
143 COMMITS=""
144 if [ -n "$CHANGELOG_SHAS" ]; then
145 COMMITS=$(git log --no-walk=unsorted --pretty=format:"- %s (%h)" --stdin <<< "$CHANGELOG_SHAS")
146 fi
147 fi
148 echo "last_release=${LAST_RELEASE:-the beginning}" >> "$GITHUB_OUTPUT"
149
150 # The changelog goes to the next step through a file, never through
151 # an expression, since commit subjects are untrusted.
152 printf '%s\n' "$COMMITS" > "$RUNNER_TEMP/changelog.md"
153
154 # Create an empty commit for the PR. The Target-Commit trailer is the
155 # source of truth for what was approved: the release workflow reads it
156 # from the proposal's head commit, and only counts approvals of that
157 # head commit, so it can't be changed after approval.
158 git config user.name "github-actions[bot]"
159 git config user.email "github-actions[bot]@users.noreply.github.com"
160 git commit --allow-empty -m "Release proposal for $VERSION" -m "Target-Commit: $COMMIT_HASH"
161
162 # Push the branch
163 git push origin "$BRANCH_NAME"
164
165 echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
166
167 - name: Create release proposal PR
168 id: create_pr
169 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
170 env:
171 VERSION: ${{ steps.inputs.outputs.version }}
172 COMMIT_HASH: ${{ steps.inputs.outputs.commit_hash }}
173 BRANCH_NAME: ${{ steps.setup.outputs.branch_name }}
174 BEHIND_INFO: ${{ steps.setup.outputs.behind_info }}
175 LAST_RELEASE: ${{ steps.setup.outputs.last_release }}
176 with:
177 script: |
178 const fs = require('fs');
179 const { proposalTitle, REQUIRED_APPROVALS } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/release-proposal.js`);
180 const { VERSION, COMMIT_HASH, BRANCH_NAME, BEHIND_INFO, LAST_RELEASE, RUNNER_TEMP } = process.env;
181 const changelog = fs.readFileSync(`${RUNNER_TEMP}/changelog.md`, 'utf8').trimEnd();
182
183 const body = [
184 `## Release Proposal: ${VERSION}`,
185 '',
186 `**Target Commit:** \`${COMMIT_HASH}\``,
187 `**Requested by:** @${context.actor}`,
188 `**Commit Status:** ${BEHIND_INFO}`,
189 '',
190 `This PR proposes creating release tag \`${VERSION}\` at commit \`${COMMIT_HASH}\`.`,
191 '',
192 '### Approval Process',
193 '',
194 `This PR requires **approval from ${REQUIRED_APPROVALS}+ maintainers** before the tag can be created.`,
195 'Approvals only count for the PR\'s current head commit, whose `Target-Commit` trailer records the commit to tag.',
196 '',
197 '### What happens next?',
198 '',
199 '1. Maintainers review this proposal',
200 `2. When ${REQUIRED_APPROVALS}+ maintainer approvals are received, an automated workflow will post tagging instructions`,
201 '3. A maintainer manually creates and pushes the signed tag',
202 '4. The release workflow is triggered automatically by the tag push, and checks the approvals again',
203 '5. Upon release completion, this PR is closed and the branch is deleted',
204 '',
205 `### Changes Since ${LAST_RELEASE}`,
206 '',
207 changelog,
208 '',
209 '### Release Checklist',
210 '',
211 '- [ ] All tests pass',
212 '- [ ] Security review completed',
213 '- [ ] Documentation updated',
214 '- [ ] Breaking changes documented',
215 '- [ ] Release notes drafted',
216 '',
217 '---',
218 '',
219 '**Note:** Tag creation is manual and requires a signed tag from a maintainer.',
220 ].join('\n');
221
222 const pr = await github.rest.pulls.create({
223 owner: context.repo.owner,
224 repo: context.repo.repo,
225 title: proposalTitle(VERSION),
226 head: BRANCH_NAME,
227 base: 'master',
228 body,
229 draft: true
230 });
231
232 // Add labels
233 await github.rest.issues.addLabels({
234 owner: context.repo.owner,
235 repo: context.repo.repo,
236 issue_number: pr.data.number,
237 labels: ['release-proposal', 'awaiting-approval']
238 });
239
240 console.log(`Created PR: ${pr.data.html_url}`);
241
242 return { number: pr.data.number, url: pr.data.html_url };
243 result-encoding: json
244
245 - name: Post summary
246 env:
247 VERSION: ${{ steps.inputs.outputs.version }}
248 COMMIT_HASH: ${{ steps.inputs.outputs.commit_hash }}
249 BEHIND_INFO: ${{ steps.setup.outputs.behind_info }}
250 PR_URL: ${{ fromJson(steps.create_pr.outputs.result).url }}
251 run: |
252 {
253 echo "## Release Proposal PR Created! 🚀"
254 echo ""
255 echo "Version: **$VERSION**"
256 echo "Commit: **$COMMIT_HASH**"
257 echo "Status: $BEHIND_INFO"
258 echo ""
259 echo "PR: $PR_URL"
260 } >> "$GITHUB_STEP_SUMMARY"