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
146 changes: 136 additions & 10 deletions .github/workflows/mainline-pr-intent.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,153 @@
name: Mainline PR Intent

on:
pull_request:
pull_request_target:
types: [opened, edited, synchronize, reopened]

permissions:
contents: read
pull-requests: write
issues: write

concurrency:
group: mainline-pr-intent-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
comment:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Inspect Mainline PR comment state
id: comment_state
uses: actions/github-script@v7
env:
EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
with:
script: |
const bodyMarker = "<!-- mainline:pr-description:start -->";
const commentMarker = "<!-- mainline:pr-comment:v1 -->";
const pr = context.payload.pull_request;
const { owner, repo } = context.repo;
const issue_number = pr.number;
const { data: livePR } = await github.rest.pulls.get({
owner,
repo,
pull_number: issue_number,
});
if (livePR.head.sha !== process.env.EXPECTED_HEAD_SHA) {
core.notice(
`Skipping stale PR event for ${process.env.EXPECTED_HEAD_SHA}; current head is ${livePR.head.sha}`
);
core.setOutput("needs_comment", "false");
return;
}
if (!(livePR.body || "").includes(bodyMarker)) {
core.setOutput("needs_comment", "true");
return;
}

const comments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number, per_page: 100 }
);
const existing = comments.find((comment) =>
comment.user.type === "Bot" &&
comment.body &&
comment.body.includes(commentMarker)
);
if (existing) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: existing.id,
});
}
core.setOutput("needs_comment", "false");

- name: Check out PR head
if: >
steps.comment_state.outputs.needs_comment == 'true' &&
github.event.pull_request.head.repo.full_name == github.repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: Check out trusted base for fork PR
if: >
steps.comment_state.outputs.needs_comment == 'true' &&
github.event.pull_request.head.repo.full_name != github.repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
persist-credentials: false

Comment on lines +82 to +86
- uses: actions/setup-go@v5
if: steps.comment_state.outputs.needs_comment == 'true'
with:
go-version-file: go.mod

- name: Retain fork PR head as Git data
if: >
steps.comment_state.outputs.needs_comment == 'true' &&
github.event.pull_request.head.repo.full_name != github.repository
id: fork_head
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
data_ref="refs/mainline/imports/pr-heads/pr-${PR_NUMBER}"
git fetch --no-tags origin \
"+refs/pull/${PR_NUMBER}/head:${data_ref}"
actual_head="$(git rev-parse "${data_ref}^{commit}")"
if [ "$actual_head" != "$HEAD_SHA" ]; then
echo "current=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping stale PR event for ${HEAD_SHA}; current head is ${actual_head}"
exit 0
fi
echo "current=true" >> "$GITHUB_OUTPUT"

- name: Generate Mainline PR intent comment
if: >
steps.comment_state.outputs.needs_comment == 'true' &&
(github.event.pull_request.head.repo.full_name == github.repository ||
steps.fork_head.outputs.current == 'true')
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
FORK_URL: ${{ github.event.pull_request.head.repo.clone_url }}
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
COMMENT_PATH: ${{ runner.temp }}/mainline-pr-comment.md
run: |
go run . pr-comment \
--base "${{ github.event.pull_request.base.sha }}" \
--head "${{ github.event.pull_request.head.sha }}" \
--branch "${{ github.event.pull_request.head.ref }}" \
> /tmp/mainline-pr-comment.md
args=(
pr-comment
--base "$BASE_SHA"
--head "$HEAD_SHA"
--branch "$HEAD_REF"
)
if [ "$IS_FORK" = "true" ]; then
args=(
--no-sync
"${args[@]}"
--pr "$PR_NUMBER"
--fork-url "$FORK_URL"
)
fi
go run . "${args[@]}" > "$COMMENT_PATH"

- name: Upsert Mainline PR intent comment
if: >
steps.comment_state.outputs.needs_comment == 'true' &&
(github.event.pull_request.head.repo.full_name == github.repository ||
steps.fork_head.outputs.current == 'true')
uses: actions/github-script@v7
env:
EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
COMMENT_PATH: ${{ runner.temp }}/mainline-pr-comment.md
with:
script: |
const fs = require("fs");
Expand All @@ -41,6 +156,17 @@ jobs:
const pr = context.payload.pull_request;
const { owner, repo } = context.repo;
const issue_number = pr.number;
const { data: livePR } = await github.rest.pulls.get({
owner,
repo,
pull_number: issue_number,
});
if (livePR.head.sha !== process.env.EXPECTED_HEAD_SHA) {
core.notice(
`Skipping stale PR event for ${process.env.EXPECTED_HEAD_SHA}; current head is ${livePR.head.sha}`
);
return;
}
const comments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number, per_page: 100 }
Expand All @@ -51,7 +177,7 @@ jobs:
comment.body.includes(commentMarker)
);

if ((pr.body || "").includes(bodyMarker)) {
if ((livePR.body || "").includes(bodyMarker)) {
if (existing) {
await github.rest.issues.deleteComment({
owner,
Expand All @@ -62,7 +188,7 @@ jobs:
return;
}

const body = fs.readFileSync("/tmp/mainline-pr-comment.md", "utf8");
const body = fs.readFileSync(process.env.COMMENT_PATH, "utf8");
if (existing) {
await github.rest.issues.updateComment({
owner,
Expand Down
12 changes: 11 additions & 1 deletion internal/cli/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ var prDescIntentID string
var prCommentBase string
var prCommentHead string
var prCommentBranch string
var prCommentPRNumber int
var prCommentForkURL string
var prImportPRNumber int
var prImportForkURL string
var prImportHeadRef string
Expand Down Expand Up @@ -61,7 +63,13 @@ var prCommentCmd = &cobra.Command{
return
}

comment, err := svc.PRComment(prCommentBase, prCommentHead, prCommentBranch)
comment, err := svc.PRCommentWithOptions(engine.PullRequestCommentOptions{
Base: prCommentBase,
Head: prCommentHead,
Branch: prCommentBranch,
PRNumber: prCommentPRNumber,
ForkURL: prCommentForkURL,
})
if err != nil {
outputError(err)
return
Expand Down Expand Up @@ -128,6 +136,8 @@ func init() {
prCommentCmd.Flags().StringVar(&prCommentBase, "base", "", "base commit SHA for the PR range")
prCommentCmd.Flags().StringVar(&prCommentHead, "head", "", "head commit SHA for the PR range")
prCommentCmd.Flags().StringVar(&prCommentBranch, "branch", "", "PR head branch name fallback")
prCommentCmd.Flags().IntVar(&prCommentPRNumber, "pr", 0, "pull request number for temporary fork refs")
prCommentCmd.Flags().StringVar(&prCommentForkURL, "fork-url", "", "fork repository URL to read contributor intent metadata from")
prImportCmd.Flags().IntVar(&prImportPRNumber, "pr", 0, "pull request number for diagnostics")
prImportCmd.Flags().StringVar(&prImportForkURL, "fork-url", "", "fork repository URL to discover actor logs from")
prImportCmd.Flags().StringVar(&prImportHeadRef, "head-ref", "", "pull request head branch name")
Expand Down
12 changes: 12 additions & 0 deletions internal/cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ func TestPRImportCommandIsRegistered(t *testing.T) {
}
}

func TestPRCommentCommandSupportsForkMetadata(t *testing.T) {
cmd, _, err := rootCmd.Find([]string{"pr-comment"})
if err != nil || cmd.Name() != "pr-comment" {
t.Fatalf("pr-comment command missing: cmd=%v err=%v", cmd, err)
}
for _, name := range []string{"base", "head", "branch", "pr", "fork-url"} {
if cmd.Flags().Lookup(name) == nil {
t.Fatalf("pr-comment missing --%s flag", name)
}
}
}

func TestPublishCommandHasForkRemoteFlag(t *testing.T) {
cmd, _, err := rootCmd.Find([]string{"publish"})
if err != nil || cmd.Name() != "publish" {
Expand Down
34 changes: 32 additions & 2 deletions internal/engine/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const (
prCommentMarker = "<!-- mainline:pr-comment:v1 -->"
)

type PullRequestCommentOptions struct {
Base string
Head string
Branch string
PRNumber int
ForkURL string
}

func (s *Service) PRDescription(intentID string) (string, error) {
if err := s.requireInit(); err != nil {
return "", err
Expand All @@ -40,16 +48,35 @@ func (s *Service) PRDescription(intentID string) (string, error) {
}

func (s *Service) PRComment(base, head, branch string) (string, error) {
return s.PRCommentWithOptions(PullRequestCommentOptions{
Base: base,
Head: head,
Branch: branch,
})
}

func (s *Service) PRCommentWithOptions(opts PullRequestCommentOptions) (string, error) {
if err := s.requireInit(); err != nil {
return "", err
}

view, _ := s.Store.ReadMainlineView()
if view == nil {
if view == nil && strings.TrimSpace(opts.ForkURL) == "" {
return formatMissingPRComment("Mainline view is not available. Run `mainline sync` and retry."), nil
}
if view == nil {
view = &domain.MainlineView{SchemaVersion: 1}
}

if strings.TrimSpace(opts.ForkURL) != "" {
forkIntents, err := s.readForkPRCommentIntents(strings.TrimSpace(opts.ForkURL), opts.PRNumber)
if err != nil {
return "", err
}
view.Intents = mergePRCommentIntents(view.Intents, forkIntents)
}

matches := s.matchPRIntents(view, base, head, branch)
matches := s.matchPRIntents(view, opts.Base, opts.Head, opts.Branch)
if len(matches) == 0 {
return formatMissingPRComment("No sealed Mainline intent was found for this PR range."), nil
}
Expand Down Expand Up @@ -143,6 +170,9 @@ func formatPRIntent(iv domain.IntentView, level int) string {
sb.WriteString(fmt.Sprintf("**Status:** `%s`\n", iv.Status))
}
sb.WriteString(fmt.Sprintf("**Title:** %s\n\n", summary.Title))
if iv.Provenance != nil && iv.Provenance.Kind == forkPRCommentProvenanceKind {
sb.WriteString("> Contributor-published intent; not yet accepted into the upstream Mainline log.\n\n")
}

sb.WriteString(subheading + " What changed\n\n")
sb.WriteString(summary.What + "\n\n")
Expand Down
Loading
Loading