Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion .github/tests/deploy-static-job-boundaries.test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,77 @@
abort "#{step_name} must serialize gcloud storage workers" unless storage_parallelism == expected_serial_execution
end

cleanup_guard = jobs.fetch("cleanup").fetch("if")
cleanup = jobs.fetch("cleanup")
cleanup_guard = cleanup.fetch("if")
abort "Preview cleanup must survive author offboarding" if cleanup_guard.include?("author_association")

cleanup_steps = cleanup.fetch("steps")
cleanup_authorization_index = cleanup_steps.index { |step| step.fetch("name", "") == "Revalidate cleanup authorization" }
cleanup_oidc_index = cleanup_steps.index { |step| step.fetch("name", "") == "Authenticate to Google Cloud" }
abort "Cleanup authorization must be revalidated immediately before OIDC" unless cleanup_authorization_index && cleanup_authorization_index + 1 == cleanup_oidc_index
cleanup_authorization = cleanup_steps.fetch(cleanup_authorization_index)
abort "Cleanup authorization recheck must use pinned GitHub Script" unless cleanup_authorization.fetch("uses", "").match?(/\Aactions\/github-script@[0-9a-f]{40}\z/)
abort "Cleanup authorization recheck must use the job token" unless cleanup_authorization.fetch("with", {}).fetch("github-token", nil) == "${{ github.token }}"
cleanup_script = cleanup_authorization.fetch("with").fetch("script")
abort "Cleanup authorization must require a closed pull request" unless cleanup_script.include?("pullRequest.state === 'closed'")
abort "Cleanup authorization must allow a write-capable closer who is not the pull-request author" if cleanup_script.include?("pullRequest.user?.login === actor")
abort "Cleanup authorization must require current write permission" unless cleanup_script.include?("['admin', 'maintain', 'write'].includes(access.permission)")

cleanup_base_pull_request = {
"state" => "closed",
"user" => { "login" => "author" },
"head" => { "repo" => { "full_name" => "CellarNode/site" } },
}
cleanup_cases = {
"write-capable maintainer closes another author's pull request" => [cleanup_base_pull_request, "write", true],
"write-capable author closes own pull request" => [cleanup_base_pull_request.merge("user" => { "login" => "maintainer" }), "write", true],
"open pull request" => [cleanup_base_pull_request.merge("state" => "open"), "write", false],
"forked head" => [cleanup_base_pull_request.merge("head" => { "repo" => { "full_name" => "attacker/site" } }), "write", false],
"revoked permission" => [cleanup_base_pull_request, "read", false],
}
cleanup_cases.each do |name, (pull_request, permission, expected)|
_stdout, _stderr, status = Open3.capture3(
{
"ACTOR" => "maintainer",
"AUTHORIZATION_SCRIPT" => cleanup_script,
"CURRENT_PERMISSION" => permission,
"PR_JSON" => JSON.generate(pull_request),
"PR_NUMBER" => "42",
"REPOSITORY" => "CellarNode/site",
},
"node", "-e", <<~'JAVASCRIPT',
const AsyncFunction = Object.getPrototypeOf(async () => null).constructor;
const pullRequest = JSON.parse(process.env.PR_JSON);
const github = {
rest: {
pulls: { get: async () => ({ data: pullRequest }) },
repos: {
getCollaboratorPermissionLevel: async ({ username }) => {
if (username !== process.env.ACTOR) throw new Error(`unexpected permission subject: ${username}`);
return { data: { permission: process.env.CURRENT_PERMISSION } };
},
},
},
};
const context = { repo: { owner: 'CellarNode', repo: 'site' } };
const core = { setFailed: message => { throw new Error(message); } };
new AsyncFunction('github', 'context', 'core', process.env.AUTHORIZATION_SCRIPT)(github, context, core)
Comment thread
mong-x marked this conversation as resolved.
.catch(error => { console.error(error.message); process.exitCode = 1; });
JAVASCRIPT
)
abort "cleanup #{name}: expected accepted=#{expected}, got accepted=#{status.success?}" unless status.success? == expected
end

delete_preview = cleanup_steps.find { |step| step.fetch("name", "") == "Delete preview" }
cleanup_parallelism = delete_preview&.fetch("env", {})&.slice(
"CLOUDSDK_STORAGE_PROCESS_COUNT",
"CLOUDSDK_STORAGE_THREAD_COUNT",
)
abort "Preview cleanup must serialize gcloud storage workers" unless cleanup_parallelism == {
"CLOUDSDK_STORAGE_PROCESS_COUNT" => "1",
"CLOUDSDK_STORAGE_THREAD_COUNT" => "1",
}

validation_path = File.expand_path("../workflows/validate-static-deploy.yaml", __dir__)
validation = YAML.safe_load(File.read(validation_path), aliases: true)
validation_checkout = validation.fetch("jobs").fetch("lock-validator").fetch("steps").find { |step| step.fetch("uses", "").start_with?("actions/checkout@") }
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/deploy-static-website.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -731,8 +731,36 @@ jobs:
permissions:
contents: read
id-token: write
pull-requests: read

steps:
- name: Revalidate cleanup authorization
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
env:
ACTOR: ${{ github.actor }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPOSITORY: ${{ github.repository }}
with:
github-token: ${{ github.token }}
script: |
const actor = process.env.ACTOR;
const prNumber = Number(process.env.PR_NUMBER);
const repository = process.env.REPOSITORY;
const { data: pullRequest } = await github.rest.pulls.get({
...context.repo,
pull_number: prNumber,
});
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({
...context.repo,
username: actor,
});
const authorized = pullRequest.state === 'closed' &&
pullRequest.head.repo?.full_name === repository &&
['admin', 'maintain', 'write'].includes(access.permission);
if (!authorized) {
core.setFailed('Cleanup authorization is no longer valid');
}

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
with:
Expand All @@ -750,6 +778,8 @@ jobs:
- name: Delete preview
env:
CLOUDSDK_CONFIG: ${{ runner.temp }}/gcloud
CLOUDSDK_STORAGE_PROCESS_COUNT: '1'
CLOUDSDK_STORAGE_THREAD_COUNT: '1'
WEBSITE_SLUG: ${{ inputs.website_slug }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
Expand Down
Loading