diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..fe9dc62056d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,135 @@ +on: + pull_request_target: + paths: + - quiche/Cargo.toml + +name: Release Checks + +permissions: + # Draft releases are only visible to tokens with push access. + contents: write + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + quiche_release_draft: + name: Check quiche release draft + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check quiche release draft + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const manifestPath = 'quiche/Cargo.toml'; + + async function getManifest(owner, repo, ref) { + const response = await github.rest.repos.getContent({ + owner, + repo, + path: manifestPath, + ref, + }); + + return Buffer.from( + response.data.content, + response.data.encoding, + ).toString('utf8'); + } + + function packageVersion(manifest) { + let inPackageSection = false; + + for (const line of manifest.split(/\r?\n/)) { + const section = line.match(/^\s*\[([^\]]+)\]\s*$/); + + if (section) { + if (section[1] === 'package') { + inPackageSection = true; + continue; + } + + if (inPackageSection) { + break; + } + + continue; + } + + if (!inPackageSection) { + continue; + } + + const version = line.match(/^\s*version\s*=\s*"([^"]+)"\s*$/); + + if (version) { + return version[1]; + } + } + + throw new Error(`No package version found in ${manifestPath}`); + } + + const baseManifest = await getManifest( + pr.base.repo.owner.login, + pr.base.repo.name, + pr.base.sha, + ); + + const headManifest = await getManifest( + pr.head.repo.owner.login, + pr.head.repo.name, + pr.head.sha, + ); + + const baseVersion = packageVersion(baseManifest); + const headVersion = packageVersion(headManifest); + + if (baseVersion === headVersion) { + core.info(`quiche version is unchanged at ${headVersion}`); + return; + } + + core.info( + `quiche version changes from ${baseVersion} to ${headVersion}`, + ); + + const releases = await github.paginate( + github.rest.repos.listReleases, + { + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }, + ); + + // quiche releases use the version as the tag name, without a crate + // prefix. Release PRs are not tagged until after they are merged, so + // check GitHub release drafts rather than git tags. + const releaseTag = headVersion; + const release = releases.find( + (release) => release.tag_name === releaseTag, + ); + + if (!release) { + core.setFailed( + `No GitHub release draft exists for quiche ${headVersion}. ` + + `Create a draft release for the future ${releaseTag} tag before ` + + `merging this release PR.`, + ); + return; + } + + if (!release.draft) { + core.setFailed( + `GitHub release ${release.html_url} exists for quiche ` + + `${headVersion}, but it is not a draft.`, + ); + return; + } + + core.info(`Found GitHub release draft: ${release.html_url}`);