diff --git a/tekton/resources/release/README.md b/tekton/resources/release/README.md index d5df6510c..7f3fd7243 100644 --- a/tekton/resources/release/README.md +++ b/tekton/resources/release/README.md @@ -82,3 +82,50 @@ tkn pipeline start \ -p rekor-uuid="$REKOR_UUID" \ release-draft ``` + +#### Using Oracle Cloud Storage + +To draft a release by downloading the release manifests directly from Oracle Cloud Storage buckets, use the [release-draft-oci](./base/github_release_oci.yaml) pipeline instead. + +If Oracle Cloud login credentials are managed in a secret called `oci-release-secret`, then set the workspace secret addordingly. + +Please note the additional inputs `--pod-template` and the new parameter `repo-name` that are specific to release pipelines with oci related tags. + +Create a pod template file: + +```shell +cat < tekton/pod-template.yaml +securityContext: + fsGroup: 65532 + runAsUser: 65532 + runAsNonRoot: true +EOF +``` +```shell +export TEKTON_RELEASE_GIT_SHA=9c884fb3d3bf35c0a251936626f2ca9f17b5c183 +export TEKTON_PACKAGE=tektoncd/pipeline +export TEKTON_VERSION=v0.9.0 +export TEKTON_OLD_VERSION=v0.8.0 +export TEKTON_RELEASE_NAME="Bengal Bender" +export TEKON_REPO_NAME=pipeline + +RELEASE_FILE=https://infra.tekton.dev/tekton-releases/pipeline/previous/${TEKTON_VERSION}/release.yaml +CONTROLLER_IMAGE_SHA=$(curl -L $RELEASE_FILE | egrep 'gcr.io.*controller' | cut -d'@' -f2) +REKOR_UUID=$(rekor-cli search --sha $CONTROLLER_IMAGE_SHA | grep -v Found | head -1) +echo -e "CONTROLLER_IMAGE_SHA: ${CONTROLLER_IMAGE_SHA}\nREKOR_UUID: ${REKOR_UUID}" + +tkn pipeline start \ + --workspace name=shared,volumeClaimTemplateFile=workspace-template.yaml \ + --workspace name=credentials,secret=oci-release-secret \ + --pod-template pod-template.yaml \ + -p package="${TEKTON_PACKAGE}" \ + -p git-revision="$TEKTON_RELEASE_GIT_SHA" \ + -p release-tag="${TEKTON_VERSION}" \ + -p previous-release-tag="${TEKTON_OLD_VERSION}" \ + -p release-name="${TEKTON_RELEASE_NAME}" \ + -p repo-name="${TEKTON_REPO_NAME}" \ + -p bucket="tekton-releases" \ + -p rekor-uuid="$REKOR_UUID" \ + release-draft-oci +``` + diff --git a/tekton/resources/release/base/github_release_oci.yaml b/tekton/resources/release/base/github_release_oci.yaml new file mode 100644 index 000000000..da5c4eea1 --- /dev/null +++ b/tekton/resources/release/base/github_release_oci.yaml @@ -0,0 +1,417 @@ +# Copyright 2025 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: create-draft-release-oci +spec: + params: + - name: package + description: package (and its children) under test + - name: git-revision + description: the git revision of the release + - name: release-name + description: The name of the release (e.g. Cat + Robot for pipeline) + - name: release-tag + description: Release number and git tag to be applied (e.g. v0.888.1, with 'v') + - name: previous-release-tag + description: Previous release number - for author and PR list calculation + - name: rekor-uuid + description: The Rekor UUID associated to the attestation + workspaces: + - name: shared + description: contains the cloned repo and the release files + stepTemplate: + env: + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: github-token + key: GITHUB_TOKEN + - name: HOME + value: /tekton/home + - name: VERSION + value: $(params.release-tag) + - name: PROJECT + value: $(params.package) + - name: OLD_VERSION + value: $(params.previous-release-tag) + - name: RELEASE_NAME + value: $(params.release-name) + - name: GIT_REVISION + value: $(params.git-revision) + - name: REKOR_UUID + value: $(params.rekor-uuid) + steps: + - name: header + image: ghcr.io/tektoncd/plumbing/hub + script: | + #!/bin/bash + set -ex + TEKTON_PROJECT=$(basename $PROJECT) + BQ="\`" # Backquote + + cat < + + -[Docs @ ${VERSION}](https://github.com/${PROJECT}/tree/${VERSION}/docs) + -[Examples @ ${VERSION}](https://github.com/${PROJECT}/tree/${VERSION}/examples) + + ## Installation one-liner + + ${BQ}${BQ}${BQ}shell + kubectl apply -f https://infra.tekton.dev/tekton-releases/${TEKTON_PROJECT}/previous/${VERSION}/release.yaml + ${BQ}${BQ}${BQ} + + ## Attestation + + The Rekor UUID for this release is \`${REKOR_UUID}\` + + Obtain the attestation: + ${BQ}${BQ}${BQ}shell + REKOR_UUID=${REKOR_UUID} + rekor-cli get --uuid \$REKOR_UUID --format json | jq -r .Attestation | jq . + ${BQ}${BQ}${BQ} + + Verify that all container images in the attestation are in the release file: + ${BQ}${BQ}${BQ}shell + RELEASE_FILE=https://infra.tekton.dev/tekton-releases/${TEKTON_PROJECT}/previous/${VERSION}/release.yaml + REKOR_UUID=${REKOR_UUID} + + # Obtains the list of images with sha from the attestation + REKOR_ATTESTATION_IMAGES=\$(rekor-cli get --uuid "\$REKOR_UUID" --format json | jq -r .Attestation | jq -r '.subject[]|.name + ":${VERSION}@sha256:" + .digest.sha256') + + # Download the release file + curl -L "\$RELEASE_FILE" > release.yaml + + # For each image in the attestation, match it to the release file + for image in \$REKOR_ATTESTATION_IMAGES; do + printf \$image; grep -q \$image release.yaml && echo " ===> ok" || echo " ===> no match"; + done + ${BQ}${BQ}${BQ} + + + + ## Changes + EOF + - name: filter-data + image: ghcr.io/tektoncd/plumbing/hub + workingDir: $(workspaces.shared.path)/repo + script: | + #!/usr/bin/env bash + set -ex + + # Fix git ownership issue - needed even when running as same user due to workspace mounting + git config --global --add safe.directory $(workspaces.shared.path)/repo + + # Restore full git history + git fetch --unshallow + git fetch --tags + + # UPPER_THRESHOLD is the newest sha we are interested in + UPPER_THRESHOLD=${GIT_REVISION} + # COMMON_ANCESTOR is the common ancestor between the OLD_VERSION and UPPER_THRESHOLD + COMMON_ANCESTOR=$(git merge-base ${OLD_VERSION} ${UPPER_THRESHOLD}) + # OLD_RELEASE_SUBJECTS is the list of commit subjects cherry-picked (probably?) from main + OLD_RELEASE_SUBJECTS=$HOME/old_subjects.txt + echo "Cherry-picked commits:" + git log --format="%s" $COMMON_ANCESTOR..$OLD_VERSION | sort -u | tee $OLD_RELEASE_SUBJECTS + echo "OLD_VERSION: $OLD_VERSION" + echo "COMMON_ANCESTOR: $COMMON_ANCESTOR" + echo "UPPER_THRESHOLD: $UPPER_THRESHOLD" + + # Save the PR data in CSV. Only consider PRs whose sha verifies the condition + # COMMON_ANCESTOR is ancestor of SHA is ancestor of UPPER_THRESHOLD + # And title no in the OLD_VERSION branch. + # Working Assumptions: + # - there are no duplicate titles in commits + # - we always cherry-pick full PRs, never commits out of a multi-commit PR + # Format of output data: + # "author;number;title" + hub pr list --state merged -L 300 -f "%sm;%au;%i;%t;%L%n" | \ + while read pr; do + SHA=$(echo $pr | cut -d';' -f1) + # Skip the common ancestor has it has already been released + if [ "$SHA" == "$COMMON_ANCESTOR" ]; then + continue + fi + SUBJECT=$(git log -1 --format="%s" $SHA || echo "__NOT_FOUND__") + git merge-base --is-ancestor $SHA $UPPER_THRESHOLD && \ + git merge-base --is-ancestor $COMMON_ANCESTOR $SHA && \ + ! $(egrep "^${SUBJECT}$" $OLD_RELEASE_SUBJECTS &> /dev/null) && + echo $pr | cut -d';' -f2- + done > $HOME/pr.csv || true # We do not want to fail is the last of the loop is not a match + + echo "$(wc -l $HOME/pr.csv | awk '{ print $1}') PRs in the new release." + cat $HOME/pr.csv + - name: release-notes + image: stedolan/jq + script: | + #!/bin/bash + set -e + + # First process pull requests that have release notes + # Extract the release notes but drop lines that match an unmodified PR template + # || true in case all PRs are "release-note-none" + grep -v "release-note-none" $HOME/pr.csv > $HOME/pr-notes-tmp.csv || true + cat $HOME/pr-notes-tmp.csv | while read pr; do + PR_NUM=$(echo $pr | cut -d';' -f2) + PR_RELEASE_NOTES_B64=$(wget -O- https://api.github.com/repos/${PROJECT}/issues/${PR_NUM:1} | \ + jq .body -r | grep -oPz '(?s)(?<=```release-note..)(.+?)(?=```)' | \ + grep -avP '\W*(Your release note here|action required: your release note here|NONE)\W*' | base64 -w0) + echo "$pr;$PR_RELEASE_NOTES_B64" >> $HOME/pr-notes.csv + # Avoid rate limiting + sleep 0.2 + done + + # Copy pull requests without release notes to a dedicated file + # || true in case no PRs have "release-note-none" + grep "release-note-none" $HOME/pr.csv > $HOME/pr-no-notes.csv || true + - name: body + image: busybox + script: | + #!/bin/sh + set -e + cat < + + + + ### Fixes + + $(awk -F";" '/kind\/bug/{ print "echo -e \"* :bug: "$3" ("$2")\n\n$(echo "$5" | base64 -d)\n\"" }' $HOME/pr-notes.csv | sh) + $(awk -F";" '/kind\/flake/{ print "echo -e \"* :bug: "$3" ("$2")\n\n$(echo "$5" | base64 -d)\n\"" }' $HOME/pr-notes.csv | sh) + $(awk -F";" '/kind\/bug/{ print "* :bug: "$3" ("$2")" }' $HOME/pr-no-notes.csv) + $(awk -F";" '/kind\/flake/{ print "* :bug: "$3" ("$2")" }' $HOME/pr-no-notes.csv) + + ### Misc + + $(awk -F";" '/kind\/cleanup/{ print "echo -e \"* :hammer: "$3" ("$2")\n\n$(echo "$5" | base64 -d)\n\"" }' $HOME/pr-notes.csv | sh) + $(awk -F";" '/kind\/misc/{ print "echo -e \"* :hammer: "$3" ("$2")\n\n$(echo "$5" | base64 -d)\n\"" }' $HOME/pr-notes.csv | sh) + $(awk -F";" '/kind\/cleanup/{ print "* :hammer: "$3" ("$2")" }' $HOME/pr-no-notes.csv) + $(awk -F";" '/kind\/misc/{ print "* :hammer: "$3" ("$2")" }' $HOME/pr-no-notes.csv) + + ### Docs + + $(awk -F";" '/kind\/documentation/{ print "echo -e \"* :book: "$3" ("$2")\n\n$(echo "$5" | base64 -d)\n\"" }' $HOME/pr-notes.csv | sh) + $(awk -F";" '/kind\/documentation/{ print "* :book: "$3" ("$2")" }' $HOME/pr-no-notes.csv) + + EOF + + - name: authors + image: ghcr.io/tektoncd/plumbing/hub + workingDir: $(workspaces.shared.path)/repo + script: | + #!/usr/bin/env bash + set -ex + cat < + EOF + - name: create-draft + image: ghcr.io/tektoncd/plumbing/hub + workingDir: $(workspaces.shared.path)/repo + script: | + #!/usr/bin/env bash + set -ex + + RELEASE_PATH="../release" + TEKTON_PROJECT=$(basename $PROJECT) + # List the files in the release folder + RELEASE_FILES=$(find "${RELEASE_PATH}" -type f | awk '{ print "-a "$1 }' | tr '\n' ' ') + + hub release create --draft --prerelease \ + --commitish ${GIT_REVISION} ${RELEASE_FILES} \ + --file $HOME/release.md ${VERSION} +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: release-draft-oci +spec: + params: + - name: package + description: package (and its children) under test + - name: repo-name + description: name of the repository (e.g. pipeline) + - name: git-revision + description: the git revision of the release + - name: release-name + description: The name of the release (e.g. Cat + Robot for pipeline) + - name: release-tag + description: Release number and git tag to be applied (e.g. v0.888.1, with 'v') + - name: previous-release-tag + description: Previous release number - for author and PR list calculation + - name: bucket + description: OCI bucket where to get the release files from (e.g. tekton-releases) + - name: rekor-uuid + description: The Rekor UUID associated to the attestation + workspaces: + - name: shared + description: Workspace where the git repo is prepared for testing + - name: credentials + description: OCI credentials + tasks: + - name: clone-repo + taskRef: + resolver: bundles + params: + - name: bundle + value: ghcr.io/tektoncd/catalog/upstream/tasks/git-clone:0.7 + - name: name + value: git-clone + - name: kind + value: task + params: + - name: url + value: https://github.com/$(params.package) + - name: revision + value: $(params.git-revision) + workspaces: + - name: output + workspace: shared + subPath: repo + - name: clone-bucket + taskRef: + resolver: bundles + params: + - name: bundle + value: ghcr.io/tektoncd/catalog/upstream/tasks/oracle-cloud-storage-download:0.2 + - name: name + value: oracle-cloud-storage-download + - name: kind + value: task + params: + - name: path + value: . + - name: bucketName + value: $(params.bucket) + - name: objectPrefix + value: $(params.repo-name)/previous/$(params.release-tag) + - name: typeDir + value: "true" + workspaces: + - name: output + workspace: shared + subPath: release + - name: credentials + workspace: credentials + - name: verify-download + runAfter: ['clone-bucket'] + taskSpec: + workspaces: + - name: shared + description: Workspace to verify + steps: + - name: check-files + image: alpine:3.20 + script: | + #!/bin/sh + set -e + + RELEASE_PATH="$(workspaces.shared.path)/release" + + echo "Checking for downloaded files in: ${RELEASE_PATH}" + + # Count files in the release directory + FILE_COUNT=$(find "${RELEASE_PATH}" -type f | wc -l) + + echo "Found ${FILE_COUNT} files" + + if [ "${FILE_COUNT}" -eq 0 ]; then + echo "ERROR: No files were downloaded from the bucket!" + echo "Contents of workspace:" + ls -la "$(workspaces.shared.path)" || true + ls -la "${RELEASE_PATH}" || true + exit 1 + fi + + echo "SUCCESS: Downloaded ${FILE_COUNT} files" + echo "Files:" + find "${RELEASE_PATH}" -type f + workspaces: + - name: shared + workspace: shared + + - name: create-draft-release + runAfter: ['clone-repo', 'verify-download'] + taskRef: + name: create-draft-release-oci + workspaces: + - name: shared + workspace: shared + params: + - name: package + value: $(params.package) + - name: git-revision + value: $(params.git-revision) + - name: release-name + value: $(params.release-name) + - name: release-tag + value: $(params.release-tag) + - name: previous-release-tag + value: $(params.previous-release-tag) + - name: rekor-uuid + value: $(params.rekor-uuid) diff --git a/tekton/resources/release/base/kustomization.yaml b/tekton/resources/release/base/kustomization.yaml index 22ff1d553..b5c863bf2 100644 --- a/tekton/resources/release/base/kustomization.yaml +++ b/tekton/resources/release/base/kustomization.yaml @@ -1,5 +1,7 @@ resources: - prerelease_checks.yaml - github_release.yaml +- github_release_oci.yaml +- prerelease-checks-oci.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization diff --git a/tekton/resources/release/base/prerelease_checks_oci.yaml b/tekton/resources/release/base/prerelease_checks_oci.yaml new file mode 100644 index 000000000..c58c4da32 --- /dev/null +++ b/tekton/resources/release/base/prerelease_checks_oci.yaml @@ -0,0 +1,141 @@ +# Copyright 2025 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: prerelease-checks-oci +spec: + params: + - name: package + description: package to release + default: github.com/tektoncd/pipeline + - name: versionTag + description: The X.Y.Z version that the artifacts would be tagged with + - name: releaseBucket + description: >- + The bucket where to look for the release, in the format / + - name: ociNamespace + description: The Oracle Cloud Infrastructure namespace for the bucket + default: "" + stepTemplate: + env: + - name: PACKAGE + value: $(params.package) + - name: VERSION_TAG + value: $(params.versionTag) + - name: RELEASE_BUCKET + value: $(params.releaseBucket) + - name: OCI_NAMESPACE + value: $(params.ociNamespace) + workspaces: + - name: source-to-release + description: The workspace where the repo has been cloned + - name: oci-credentials + description: The workspace containing OCI credentials secret + optional: false + steps: + - name: check-git-tag + image: alpine/git + script: | + echo "Checking git tag" + # Look for the tag in the list of tags + git ls-remote --tags https://$(params.package) | \ + grep "${VERSION_TAG}$" || exit 0 + # If the version was found fail + echo "Version ${VERSION_TAG} already tagged for ${PACKAGE}" + exit 1 + - name: check-release-file + image: ghcr.io/oracle/oci-cli:sha-b324bd6 # released 2025-10-01 + script: | + #!/bin/bash + set -e + echo "Checking release file" + + # Create OCI CLI config directory + mkdir -p /oracle/.oci + chmod 700 /oracle/.oci + + # Read credentials from workspace files directly into config + # Avoid storing sensitive data in environment variables + OCI_FINGERPRINT=$(cat $(workspaces.oci-credentials.path)/fingerprint) + OCI_TENANCY_OCID=$(cat $(workspaces.oci-credentials.path)/tenancy_ocid) + OCI_USER_OCID=$(cat $(workspaces.oci-credentials.path)/user_ocid) + OCI_REGION=$(cat $(workspaces.oci-credentials.path)/region) + + # Copy the API key directly (avoid echoing or storing in variables) + cp $(workspaces.oci-credentials.path)/oci_api_key.pem /oracle/.oci/oci_api_key.pem + chmod 600 /oracle/.oci/oci_api_key.pem + + # Create OCI config file + cat > /oracle/.oci/config </dev/null; then + echo "Release file already exists for ${VERSION_TAG} in the release bucket," + echo "but no git tag was found. To continue remove the release file first." + # Clean up credentials before exiting + rm -f /oracle/.oci/oci_api_key.pem /oracle/.oci/config + exit 1 + fi + + # Clean up credentials + rm -f /oracle/.oci/oci_api_key.pem /oracle/.oci/config + echo "Release file does not exist. Check passed." + - name: check-github-release + image: python:3.6-alpine3.9 + script: | + echo "Checking GitHub release" + PACKAGE=$(echo ${PACKAGE} | cut -d/ -f2,3) + # Check if the release exists on GitHub + wget -q -O- --header 'Accept: application/vnd.github.v3+json' \ + https://api.github.com/repos/${PACKAGE}/releases | \ + python -c 'import sys; import json; print("\n".join([x["tag_name"] for x in json.load(sys.stdin)]))' | \ + grep "${VERSION_TAG}$" || exit 0 + echo "Release ${VERSION_TAG} already exists for ${PACKAGE}" + exit 1 + - name: success-confirmation + image: alpine + script: | + echo "All pre-release checks for ${PACKAGE} @ ${VERSION_TAG} where successful" + echo "Happy releasing 😺" \ No newline at end of file