saschagrunert
mail@
.de
36.000 individual contributors
77.000 members on slack
1.200 companies involved
API Machinery
Apps
Architecture
Auth
Autoscaling
CLI
Cloud Provider
Storage
Service Catalog
PM
Node
Network
Multicluster
Instrumentation
Docs
Contributor Experience
Cluster Lifecycle
Release
Scalability
Scheduling
Testing
UI
Usability
Windows
delivers a new kubernetes release every 3 month
around 40 people with different roles
Lead
Enhancements
CI Signal
Bug Triage
Docs
Release Notes
Communications
Patch Release Team
Branch Managers
Release Manager Associates
SIG Release Chairs
Associates
deliver the final kubernetes release notes
provide continuous release notes updates during the release cycle
works together with other SIGs to collect major features and known issues
written in Angular 8
and TypeScript
backend-less hosted on netlify
continuously integrated via prow
unit-tested by Jest
end-to-end tested by cypress
Kubernetes based CI/CD system
provides GitHub automation in the form of policy enforcement, chat-ops via /foo style commands, and automatic PR merging
automatic issue labeling
workflow management via approval and lgtm
automatic merge tide
Graduate Volume Expansion to Beta + e2e tests #81467
go run cmd/release-notes/main.go
--output notes.json
--format json
--release-version 1.16.0
--start-rev v1.15.0
--end-rev v1.16.0
--github-token <my-secret-token>
the release notes tool is able to generate either Markdown or JSON
automatic generation for every patch release
{
"81467": {
"commit": "dccd28269a6596bf2f0041c14dd3233be8cfa571",
"text": "Move CSI volume expansion to beta.",
"markdown": "Move CSI volume expansion to beta. ([#81467](https://github.com/kubernetes/kubernetes/pull/81467), [@bertinatto](https://github.com/bertinatto))\n\n Courtesy of SIG Testing",
"documentation": [
{
"url": "https://github.com/kubernetes/enhancements/issues/556",
"type": "KEP"
}
],
"author": "bertinatto",
"author_url": "https://github.com/bertinatto",
"pr_url": "https://github.com/kubernetes/kubernetes/pull/81467",
"pr_number": 81467,
"areas": ["test"],
"kinds": ["feature"],
"sigs": ["testing"],
"feature": true,
"release_version": "1.16.0"
}
}
over 3.600 commits per release
around 300 release notes per release
export const assets = [
'assets/release-notes-1.16.json',
'assets/release-notes-1.15.4.json',
'assets/release-notes-1.15.3.json',
'assets/release-notes-1.15.2.json',
'assets/release-notes-1.15.1.json',
'assets/release-notes-1.15.json',
];
src/environments/assets.ts
@Injectable({
providedIn: 'root',
})
export class NotesService {
constructor(private http: HttpClient, private logger: LoggerService) {}
/**
* Retrieve the notes
*
* @returns The NoteList as observable
*/
getNotes(): Observable<Note[]> {
this.logger.debug(`Gathering notes from ${assets.length} assets`);
const observables = [];
for (const asset of assets) {
observables.push(this.http.get(asset));
}
return forkJoin(observables).pipe(map(this.toNoteList));
}
/**
* Convert an array of any objects to a list of notes
*
* @returns The Note list
*/
toNoteList(jsonArray: any[]): Note[] {
const list = [];
for (let i = 0, len = jsonArray.length; i < len; i++) {
for (const value of Object.values(jsonArray[i])) {
list.push(value);
}
}
return list;
}
}
@Component({
selector: 'app-notes',
templateUrl: './notes.component.html',
providers: [],
styleUrls: ['./notes.component.scss'],
})
export class NotesComponent {
filter: Filter = new Filter();
allNotes: Note[] = [];
constructor(private store: Store<State>) {
this.store.dispatch(new GetNotes());
// ...
}
}
@Injectable()
export class NotesEffects {
@Effect()
getNotes$ = this.actions$.pipe(
ofType(ActionTypes.GetNotes),
exhaustMap(() =>
this.notesService.getNotes().pipe(
map((notes: Note[]) => {
this.logger.debug('[Notes Effects:GetNotes] SUCCESS');
return new GetNotesSuccess(notes);
}),
catchError(error => {
this.logger.debug(`[Notes Effects:GetNotes] FAILED: ${error}`);
return of(new Failed(error));
}),
),
),
);
}
/**
* All available option types
*/
export enum OptionType {
areas = 'areas',
kinds = 'kinds',
releaseVersions = 'releaseVersions',
sigs = 'sigs',
documentation = 'documentation',
}
/**
* The option data type
*/
export type OptionData = Map<OptionType, OptionSet>;
/**
* The options for a single OptionType
*/
export type OptionSet = Set<string>;
/**
* A generic options abstraction
*/
export class Options {
/**
* The private data store
*/
private store: OptionData = new Map([
[OptionType.areas, new Set()],
[OptionType.kinds, new Set()],
[OptionType.releaseVersions, new Set()],
[OptionType.sigs, new Set()],
[OptionType.documentation, new Set()],
]);
/**
* Retrieve the data
*
* @returns OptionData The currently available data
*/
public get data(): OptionData {
return this.store;
}
/**
* Retrieve an single OptionSet for the provided optionType
*
* @param optionType The OptionType to be used
*
* @returns OptionSet The data
*/
public get(optionType: OptionType): OptionSet {
return this.data.get(optionType);
}
/**
* Append an input string array to the provided option type by discarding
* duplicate elements
*
* @param optionType The OptionType to be used
* @param input The array of documentation string to be added
*/
public add(optionType: OptionType, input: string[]) {
this.data.set(optionType, this.merge(this.data.get(optionType), input));
}
/**
* Merge a set and a string array into a new set
*
* @param input The base set
* @param arr The array to be appended
*
* @returns OptionSet the new Set of strings
*/
private merge(input: OptionSet, arr: string[]): OptionSet {
return new Set([...input, ...new Set(arr)]);
}
}
import { Params } from '@angular/router';
import { Options, OptionType, OptionSet } from './options.model';
/**
* A generic filter abstraction based on the Options type
*/
export class Filter extends Options {
/**
* The filter text
*/
public text = '';
/**
* Specifies of the filter should hide pre release versions
*/
public displayPreReleases = false;
/**
* The markdown key for the filter text
*/
public readonly markdownKey = 'markdown';
/**
* Helper method to test if the filter is empty
*
* @returns true is the filter is empty, false otherwise
*/
public isEmpty(): boolean {
if (this.text.length > 0) {
return false;
}
let empty = true;
this.data.forEach((value: OptionSet, key: string) => {
if (key !== OptionType.releaseVersions && value.size > 0) {
empty = false;
}
});
return empty;
}
/**
* Helper method to convert filter object into a URI-friendly object
*
* @returns Params the query parameter object
*/
public toURI(): Params {
const params = {};
// Set the markdown if needed
if (this.text.trim().length > 0) {
params[this.markdownKey] = this.text;
}
// Add the option data if needed
this.data.forEach((value: OptionSet, key: string) => {
if (value.size > 0) {
params[key] = [...value.values()];
}
});
return params;
}
/**
* Method to see if an option is empty
*
* @returns boolean true if empty, otherwise false
*/
public optionIsEmpty(optionType: OptionType): boolean {
if (super.get(optionType).size === 0) {
return true;
}
return false;
}
/**
* Method to add a value to an OptionType
*
* @param optionType The OptionType to be used
* @param value The value to be added
*/
public set(optionType: OptionType, value: string): void {
super.get(optionType).add(value);
}
/**
* Method to delete a value for an OptionType
*
* @param optionType The OptionType to be used
* @param value The value to be deleted
*/
public del(optionType: OptionType, value: string): void {
super.get(optionType).delete(value);
}
/**
* Method to check if the option type and value exists whtin the filter
*
* @param optionType The OptionType to be used
* @param value The value to be checked
*
* @returns boolean true if the filter contains the value, otherwise false
*/
public has(optionType: OptionType, value: string): boolean {
return super.get(optionType).has(value);
}
/**
* Method to check if the option type and value exists whtin the filter
*
* @param optionType The OptionType to be used
* @param values The values to be checked
*
* @returns boolean true if the filter contains any of the values, otherwise false
*/
public hasAny(optionType: OptionType, values: string[]): boolean {
if (values) {
for (const v of values) {
if (this.has(optionType, v)) {
return true;
}
}
}
return false;
}
/**
* Test wheter a release version string is a pre release
*
* @param version The release version string
*
* @returns boolean true if it's a pre-release, otherwise false
*/
public isPreRelease(version: string): boolean {
if (version) {
return version.includes('alpha') ||
version.includes('beta') ||
version.includes('rc');
}
return false;
}
}
full automation between drafting a release and the release notes website
features like RSS feeds, marking notes as read or a dedicated filter syntax
relnotes.k8s.io
github.com/kubernetes-sigs/release-notes
github.com/kubernetes/sig-release
#sig-release #release-notes on slack.k8s.io