Skip to content

Commit 441117a

Browse files
authored
Merge branch 'master' into pront-vdev-script-args
2 parents 870ae69 + d9614af commit 441117a

23 files changed

Lines changed: 795 additions & 56 deletions

File tree

‎.github/ISSUE_TEMPLATE/minor-release.md‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@ export RELEASE_BRANCH="v${NEW_VECTOR_VERSION%.*}"
1616

1717
- [ ] Cut a new release of [VRL](https://github.com/vectordotdev/vrl) if needed.
1818
- VRL release steps: https://github.com/vectordotdev/vrl/blob/main/release/README.md
19-
- [ ] Set the `Release freeze` ruleset to **Active** in [repository rulesets](https://github.com/vectordotdev/vector/settings/rules).
2019
- [ ] Run the [Prepare release](https://github.com/vectordotdev/vector/actions/workflows/release_prepare.yml)
2120
workflow from `master` with `version` set to the stable Vector version and `vrl_version` to the exact released VRL version.
21+
- The workflow activates the `RELEASE_FREEZE_RULESET_ID` ruleset and grants `vectordotdev-bot` an **Always** bypass
22+
to some of `master`'s rulesets (`RELEASE_FREEZE_BOT_BYPASS`).
23+
- If preparation fails after activation, the freeze remains active. Retry or use the closeout steps below to unfreeze the repository.
2224
- [ ] Review the bot-authored `prepare-v-<major>-<minor>-<patch>-website` PR: edit the release description,
2325
changelog, upgrade guidance, and release date as needed. Review deprecations with
2426
`cargo vdev deprecation show --version "${NEW_VECTOR_VERSION}"`.
2527

26-
Keep the freeze active until housekeeping has merged. Maintainers with bypass access must also
27-
respect this window: do not merge unrelated PRs into `master`.
28+
Keep the freeze active until **both** the release workflow's housekeeping PR has
29+
merged **and** the Kubernetes manifests push below has completed. Maintainers with
30+
bypass access must also respect this window: do not merge unrelated PRs into `master`.
2831

2932
# Publish the release
3033

@@ -53,5 +56,12 @@ The tag starts the release workflow; do not create the tag or release branch man
5356

5457
- [ ] Wait for the release workflow to merge its housekeeping PR after checks pass.
5558
It begins the next minor `-dev` version, restores VRL `main`, and refreshes licenses and documentation.
56-
- [ ] Set the `Release freeze` ruleset back to **Disabled**.
57-
- [ ] Run `cargo vdev build manifests` after the Helm chart release and open a separate PR with the changes.
59+
- [ ] Wait for the Helm chart release to push the Kubernetes manifests directly to `master`.
60+
- The chart release triggers [Refresh Kubernetes manifests](https://github.com/vectordotdev/vector/actions/workflows/release_manifests.yml),
61+
which runs `cargo vdev build manifests` and, when the generated manifests differ,
62+
commits and pushes them to `master` itself as the `vectordotdev-bot` — no PR, no review,
63+
no merge queue. If the run reports no changes, the manifests already match the chart.
64+
- [ ] Close out the direct-push window **before** disabling the freeze:
65+
- [ ] Remove the temporary `vectordotdev-bot` **Always** bypass from every
66+
ruleset in `RELEASE_FREEZE_BOT_BYPASS`.
67+
- [ ] Set the `RELEASE_FREEZE_RULESET_ID` ruleset back to **Disabled**.

‎.github/ISSUE_TEMPLATE/patch-release.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,17 @@ export PREP_BRANCH=prepare-v-0-"${CURRENT_MINOR_VERSION}"-"${NEW_PATCH_VERSION}"
6161
- The Vector release workflow starts [Helm release preparation](https://github.com/vectordotdev/helm-charts/actions/workflows/release-prepare.yml)
6262
automatically for the latest stable Vector release.
6363
- See [releasing Helm chart](https://github.com/vectordotdev/helm-charts/blob/develop/RELEASING.md) for the review steps.
64-
- [ ] Once Helm chart is released, updated Vector manifests
65-
- Run `cargo vdev build manifests` and open a PR with changes
64+
- [ ] Once the Helm chart is released, wait for the Kubernetes manifests push to `master`.
65+
- The chart release triggers [Refresh Kubernetes manifests](https://github.com/vectordotdev/vector/actions/workflows/release_manifests.yml),
66+
which runs `cargo vdev build manifests` and, when the generated manifests differ,
67+
commits and pushes them to `master` itself as the `vectordotdev-bot` — no PR, no review,
68+
no merge queue. If the run reports no changes, the manifests already match the chart.
69+
- [ ] After the manifests push succeeds, close out the direct-push window **before**
70+
disabling the freeze:
71+
- [ ] Remove the temporary `vectordotdev-bot` **Always** bypass from every
72+
ruleset in `RELEASE_FREEZE_BOT_BYPASS`.
73+
- [ ] Set the `RELEASE_FREEZE_RULESET_ID` ruleset back to **Disabled**.
74+
6675
- [ ] Cherry-pick any release commits from the release branch that are not on `master`, to `master`
6776
- [ ] Wait for the release workflow to reset the `website` branch to the release commit
6877
(`refs/heads/website` is force-pushed to the release branch HEAD) to update
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
name: Activate release freeze
2+
description: Freeze master and grant the release bot temporary direct-push bypasses.
3+
inputs:
4+
app-client-id:
5+
description: Client ID of the vectordotdev-bot GitHub App, which requires Administration write permission.
6+
required: true
7+
app-private-key:
8+
description: Private key of the vectordotdev-bot GitHub App.
9+
required: true
10+
freeze-ruleset-id:
11+
description: Repository variable RELEASE_FREEZE_RULESET_ID (the vars context is unavailable in composite actions).
12+
required: true
13+
bot-bypass-ruleset-ids:
14+
description: Repository variable RELEASE_FREEZE_BOT_BYPASS, comma-separated ruleset IDs (the vars context is unavailable in composite actions).
15+
required: true
16+
runs:
17+
using: composite
18+
steps:
19+
# Keep ruleset administration separate from tokens used to push release commits.
20+
- name: Create Vector bot token for ruleset updates
21+
id: bot-token
22+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
23+
with:
24+
client-id: ${{ inputs.app-client-id }}
25+
private-key: ${{ inputs.app-private-key }}
26+
permission-administration: write
27+
permission-contents: read
28+
29+
- name: Activate freeze and configure bot bypasses
30+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
31+
env:
32+
BOT_APP_SLUG: ${{ steps.bot-token.outputs.app-slug }}
33+
RELEASE_FREEZE_RULESET_ID: ${{ inputs.freeze-ruleset-id }}
34+
RELEASE_FREEZE_BOT_BYPASS: ${{ inputs.bot-bypass-ruleset-ids }}
35+
with:
36+
github-token: ${{ steps.bot-token.outputs.token }}
37+
script: |
38+
if (process.env.BOT_APP_SLUG !== "vectordotdev-bot") {
39+
throw new Error("Release freeze activation requires the vectordotdev-bot GitHub App.");
40+
}
41+
const { readRulesetIds, isBotActor, hasBotBypass, restrictsMasterUpdates, canManageReleaseBypass } =
42+
require("./.github/actions/require-release-freeze/rulesets.js");
43+
const { freezeId, bypassIds } = readRulesetIds(process.env);
44+
const { data: app } = await github.request("GET /apps/{app_slug}", {
45+
app_slug: process.env.BOT_APP_SLUG,
46+
});
47+
const { data: repository } = await github.request("GET /repos/{owner}/{repo}", context.repo);
48+
const branchRules = () => github.paginate(
49+
"GET /repos/{owner}/{repo}/rules/branches/{branch}",
50+
{ ...context.repo, branch: "master" },
51+
);
52+
const rules = await branchRules();
53+
// Include configured IDs even when disabled or not effective on master,
54+
// so invalid configuration fails instead of silently skipping a ruleset.
55+
const ids = new Set([freezeId, ...bypassIds, ...rules.map((rule) => rule.ruleset_id)]);
56+
const details = await Promise.all([...ids].map(async (id) => {
57+
const { data } = await github.request(
58+
"GET /repos/{owner}/{repo}/rulesets/{ruleset_id}",
59+
{ ...context.repo, ruleset_id: id, includes_parents: true },
60+
);
61+
return data;
62+
}));
63+
const byId = new Map(details.map((ruleset) => [ruleset.id, ruleset]));
64+
const freeze = byId.get(freezeId);
65+
if (!["active", "disabled"].includes(freeze.enforcement) ||
66+
!restrictsMasterUpdates(freeze, repository.default_branch) || !hasBotBypass(freeze, app.id)) {
67+
throw new Error(`Ruleset ${freezeId} must restrict master updates and already permit direct bot pushes.`);
68+
}
69+
for (const type of ["non_fast_forward", "deletion"]) {
70+
if (!rules.some((rule) => rule.type === type && byId.get(rule.ruleset_id)?.bypass_actors?.length === 0)) {
71+
throw new Error(`master must retain an active ${type} rule without bypass actors.`);
72+
}
73+
}
74+
75+
const pushSafe = new Set(["creation", "deletion", "non_fast_forward", "required_linear_history"]);
76+
const updates = [];
77+
for (const id of bypassIds) {
78+
const ruleset = byId.get(id);
79+
// A ruleset bypass applies to every rule and branch in that ruleset.
80+
// Do not broaden it to other branches, inherited policy, or safety rules.
81+
if (!canManageReleaseBypass(ruleset, repository.default_branch) || !rules.some((rule) => rule.ruleset_id === id)) {
82+
throw new Error(`Cannot safely grant a release bypass to configured ruleset ${id} (${ruleset.name}). It must be active and contain only master release policy.`);
83+
}
84+
if (!Array.isArray(ruleset.bypass_actors)) {
85+
throw new Error(`Cannot read bypass actors for ruleset ${id} (${ruleset.name}).`);
86+
}
87+
if (!hasBotBypass(ruleset, app.id)) {
88+
updates.push(ruleset);
89+
}
90+
}
91+
for (const rule of rules) {
92+
if (!pushSafe.has(rule.type) && rule.ruleset_id !== freezeId &&
93+
!bypassIds.includes(rule.ruleset_id) && !hasBotBypass(byId.get(rule.ruleset_id), app.id)) {
94+
throw new Error(`Ruleset ${rule.ruleset_id} blocks direct pushes but is missing from RELEASE_FREEZE_BOT_BYPASS.`);
95+
}
96+
}
97+
98+
// Freeze first. If a later update fails, leave master frozen; a retry
99+
// completes the remaining bypasses without duplicating existing actors.
100+
// Only toggle enforcement: Release freeze keeps its existing bypass actors.
101+
if (freeze.enforcement !== "active") {
102+
await github.request("PUT /repos/{owner}/{repo}/rulesets/{ruleset_id}", {
103+
...context.repo,
104+
ruleset_id: freeze.id,
105+
enforcement: "active",
106+
});
107+
}
108+
if (!(await branchRules()).some((rule) => rule.ruleset_id === freeze.id && rule.type === "update")) {
109+
throw new Error("Release freeze must restrict updates to master before granting bot bypasses.");
110+
}
111+
for (const ruleset of updates) {
112+
// Update only the bot's bypass, never the freeze or safety policies.
113+
await github.request("PUT /repos/{owner}/{repo}/rulesets/{ruleset_id}", {
114+
...context.repo,
115+
ruleset_id: ruleset.id,
116+
bypass_actors: [
117+
...ruleset.bypass_actors.filter((actor) => !isBotActor(actor, app.id)),
118+
{ actor_id: app.id, actor_type: "Integration", bypass_mode: "always" },
119+
],
120+
});
121+
core.info(`Granted release bypass on ruleset ${ruleset.id} (${ruleset.name}).`);
122+
}
123+
124+
# Check effective permissions with a contents-only token, not the privileged
125+
# administration token used above. This also checks inherited rulesets.
126+
- name: Verify active release freeze
127+
uses: ./.github/actions/require-release-freeze
128+
with:
129+
app-client-id: ${{ inputs.app-client-id }}
130+
app-private-key: ${{ inputs.app-private-key }}
131+
freeze-ruleset-id: ${{ inputs.freeze-ruleset-id }}
132+
bot-bypass-ruleset-ids: ${{ inputs.bot-bypass-ruleset-ids }}
Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,111 @@
11
name: Require active release freeze
2-
description: Fail unless the repository's Release freeze ruleset is active.
2+
description: Require an active master freeze, bot direct-push access, and force-push protection.
3+
inputs:
4+
app-client-id:
5+
description: Client ID of the vectordotdev-bot GitHub App.
6+
required: true
7+
app-private-key:
8+
description: Private key of the vectordotdev-bot GitHub App.
9+
required: true
10+
freeze-ruleset-id:
11+
description: Repository variable RELEASE_FREEZE_RULESET_ID (the vars context is unavailable in composite actions).
12+
required: true
13+
bot-bypass-ruleset-ids:
14+
description: Repository variable RELEASE_FREEZE_BOT_BYPASS, comma-separated ruleset IDs (the vars context is unavailable in composite actions).
15+
required: true
316
runs:
417
using: composite
518
steps:
19+
# Query as the bot: GITHUB_TOKEN cannot prove the bot's bypass permissions,
20+
# and reading bypass_actors requires ruleset write access.
21+
- name: Create Vector bot token for freeze checks
22+
id: bot-token
23+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
24+
with:
25+
client-id: ${{ inputs.app-client-id }}
26+
private-key: ${{ inputs.app-private-key }}
27+
permission-contents: write
28+
permission-administration: read
29+
630
# This is a best-effort guard and remains susceptible to TOCTOU changes after the check.
7-
# Maintainers must respect the release freeze window until housekeeping has merged.
8-
- name: Check release freeze ruleset
31+
# Keep the freeze and temporary bot bypasses until housekeeping and manifests finish.
32+
- name: Check release freeze and bot permissions
933
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
34+
env:
35+
BOT_APP_SLUG: ${{ steps.bot-token.outputs.app-slug }}
36+
RELEASE_FREEZE_RULESET_ID: ${{ inputs.freeze-ruleset-id }}
37+
RELEASE_FREEZE_BOT_BYPASS: ${{ inputs.bot-bypass-ruleset-ids }}
1038
with:
39+
github-token: ${{ steps.bot-token.outputs.token }}
1140
script: |
12-
const rulesets = await github.paginate(
13-
"GET /repos/{owner}/{repo}/rulesets",
14-
context.repo,
41+
if (process.env.BOT_APP_SLUG !== "vectordotdev-bot") {
42+
core.setFailed("Release freeze checks require the vectordotdev-bot GitHub App.");
43+
return;
44+
}
45+
const { readRulesetIds, restrictsMasterUpdates, canManageReleaseBypass } =
46+
require("./.github/actions/require-release-freeze/rulesets.js");
47+
const { freezeId, bypassIds } = readRulesetIds(process.env);
48+
const { data: repository } = await github.request("GET /repos/{owner}/{repo}", context.repo);
49+
50+
// Let GitHub resolve branch patterns and inherited rulesets. An active
51+
// freeze targeting a different branch does not protect master.
52+
const rules = await github.paginate(
53+
"GET /repos/{owner}/{repo}/rules/branches/{branch}",
54+
{ ...context.repo, branch: "master" },
1555
);
16-
const matches = rulesets.filter(
17-
(ruleset) => ruleset.name === "Release freeze",
56+
const details = await Promise.all(
57+
[...new Set([freezeId, ...bypassIds, ...rules.map((rule) => rule.ruleset_id)])].map(async (id) => {
58+
const { data } = await github.request(
59+
"GET /repos/{owner}/{repo}/rulesets/{ruleset_id}",
60+
{ ...context.repo, ruleset_id: id, includes_parents: true },
61+
);
62+
return data;
63+
}),
1864
);
19-
if (matches.length !== 1) {
20-
core.setFailed(
21-
`Expected exactly one Release freeze ruleset, found ${matches.length}.`,
22-
);
65+
const byId = new Map(details.map((ruleset) => [ruleset.id, ruleset]));
66+
const freeze = byId.get(freezeId);
67+
if (freeze.enforcement !== "active" || !restrictsMasterUpdates(freeze, repository.default_branch) ||
68+
!rules.some((rule) => rule.ruleset_id === freezeId && rule.type === "update")) {
69+
core.setFailed(`Configured freeze ruleset ${freezeId} must actively restrict master updates.`);
2370
return;
2471
}
25-
if (matches[0].enforcement !== "active") {
26-
core.setFailed("The Release freeze ruleset must stay active until housekeeping merges.");
72+
const modes = new Set(["always", "exempt", "pull_requests_only", "never"]);
73+
for (const ruleset of details) {
74+
if (!modes.has(ruleset.current_user_can_bypass)) {
75+
core.setFailed(`Cannot determine vectordotdev-bot bypass access for ${ruleset.name}.`);
76+
return;
77+
}
78+
}
79+
const canPush = (ruleset) => ["always", "exempt"].includes(ruleset.current_user_can_bypass);
80+
for (const id of [freezeId, ...bypassIds]) {
81+
const ruleset = byId.get(id);
82+
if (!canPush(ruleset)) {
83+
core.setFailed(`Configured ruleset ${id} must permit direct pushes by vectordotdev-bot.`);
84+
return;
85+
}
86+
}
87+
for (const id of bypassIds) {
88+
if (!canManageReleaseBypass(byId.get(id), repository.default_branch) ||
89+
!rules.some((rule) => rule.ruleset_id === id)) {
90+
core.setFailed(`Configured bypass ruleset ${id} must be active and contain only master release policy.`);
91+
return;
92+
}
93+
}
94+
95+
// These rules permit an ordinary, single-parent update. All other
96+
// effective rules require a direct-push bypass, not a PR-only bypass.
97+
const pushSafe = new Set(["creation", "deletion", "non_fast_forward", "required_linear_history"]);
98+
for (const ruleset of details) {
99+
const blocking = rules.filter((rule) => rule.ruleset_id === ruleset.id && !pushSafe.has(rule.type));
100+
if (blocking.length > 0 && !canPush(ruleset)) {
101+
core.setFailed(
102+
`vectordotdev-bot needs an always bypass for ${ruleset.name} during the release freeze (${blocking.map((rule) => rule.type).join(", ")}). A pull-request-only bypass does not allow direct pushes.`,
103+
);
104+
}
105+
}
106+
// Bypassing PR/check rules must not also permit rewriting or deleting master.
107+
for (const type of ["non_fast_forward", "deletion"]) {
108+
if (!rules.some((rule) => rule.type === type && !canPush(byId.get(rule.ruleset_id)))) {
109+
core.setFailed(`master must retain an active ${type} rule that vectordotdev-bot cannot bypass.`);
110+
}
27111
}

0 commit comments

Comments
 (0)