kubernetes

Release Notes Website

About Me

saschagrunert

mail@

.de

kubernetes

Governance

How to handle a project with

36.000 individual contributors

77.000 members on slack

1.200 companies involved

Special Interest Groups (SIGs)

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

SIG Release

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

Release Notes Team

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

The Website

relnotes.k8s.io

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

Prow?

Kubernetes based CI/CD system

provides GitHub automation in the form of policy enforcement, chat-ops via /foo style commands, and automatic PR merging

Prow welcomes new contributors

automatic issue labeling

workflow management via approval and lgtm

automatic merge tide

Prow tests everything

Prow rebases before merging

From the Pull Request to the Website

Create a PR in kubernetes/kubernetes

Provide additional documentation if necessary

The final PR

Prow automatically labels and requests review

Graduate Volume Expansion to Beta + e2e tests #81467

Release Notes generation

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"
  }
}

Consumable JSON

Release based static assets

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

Mix between Redux and service based architecture

@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());
	// ...
  }
}

Notes retrieval and filtering via Redux

@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));
        }),
      ),
    ),
  );
}

Options abstraction

/**
 * 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)]);
  }
}

Filter abstraction

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;
  }
}

Future Plans

full automation between drafting a release and the release notes website

features like RSS feeds, marking notes as read or a dedicated filter syntax

That’s it.

relnotes.k8s.io

github.com/kubernetes-sigs/release-notes

github.com/kubernetes/sig-release

#sig-release #release-notes on slack.k8s.io

The Kubernetes Release Notes Website

By Sascha Grunert

The Kubernetes Release Notes Website

A talk about the Kubernetes Release Notes tooling and website.

  • 974