diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index ddc3410..7573c7d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Reusable GitHub Actions for Sourcegraph repositories. | Action | Description | |--------|-------------| | [go-setup](./go-setup) | Setup Go with private Sourcegraph repository access | +| [diff-tour](./diff-tour) | Generate a link to the Diff Tour for a PR | ## Usage diff --git a/diff-tour/README.md b/diff-tour/README.md new file mode 100644 index 0000000..8450019 --- /dev/null +++ b/diff-tour/README.md @@ -0,0 +1,35 @@ +# `diff-tour` + +Generates a link to the Diff Tour for a PR and posts it as a PR comment. + +## Usage + +```yaml +on: + pull_request: + # `closed` fires on merge, so the comment is updated to link the merge commit + types: [opened, synchronize, reopened, closed] + +permissions: + pull-requests: write + +jobs: + diff-tour: + runs-on: ubuntu-latest + # Diff Tour resolves branch names against the base repo, so skip fork PRs. + # Closed PRs only need an update when they were merged. + if: > + github.event.pull_request.head.repo.full_name == github.repository + && (github.event.action != 'closed' || github.event.pull_request.merged) + steps: + - uses: sourcegraph/actions/diff-tour@main +``` + +## Inputs + +This action has no inputs. + +## What it does + +1. Builds a Diff Tour URL for the PR — a commit link if the PR is merged, or a branch compare link if it's still open +2. Creates a PR comment with the link, or updates the existing one if it has already posted a comment diff --git a/diff-tour/action.yaml b/diff-tour/action.yaml new file mode 100644 index 0000000..924f0af --- /dev/null +++ b/diff-tour/action.yaml @@ -0,0 +1,13 @@ +name: Diff Tour link +description: Generate a link to the Diff Tour based on a PR. + +runs: + using: composite + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const script = require(`${{ github.action_path }}/generateTourLink.cjs`) + const result = await script({ github, context }) + console.log(result) diff --git a/diff-tour/generateTourLink.cjs b/diff-tour/generateTourLink.cjs new file mode 100644 index 0000000..c0764e4 --- /dev/null +++ b/diff-tour/generateTourLink.cjs @@ -0,0 +1,108 @@ +/** + * @file This file is written in plain JavaScript to remove an extra build step, but we should use the @ts-check + * directive and JSDoc comments to make sure the logic is still type-safe. + * + * @todo 2026-09-10 - This file recreates local versions of the `AsyncFunctionArguments` type from + * `@actions/github-script`.so that we can keep this file fully self-contained and type-safe without having to bring in + * typical JS/TS tooling. If this repo gets to the point where it needs multiple JS scripts, consider ripping out these + * local types in favor of bringing in the package straight from GitHub. + */ +//@ts-check + +/** @type {{log: (...args: unknown[]) => void}} */ +const console = /** @type {any} */ (globalThis).console + +/** + * @typedef {object} PullRequest + * @property {number} number + * @property {boolean} merged + * @property {string} merge_commit_sha + * @property {{ref: string}} base + * @property {{ref: string}} head + */ + +/** + * @typedef {object} Context + * @property {{pull_request?: PullRequest}} payload + * @property {{owner: string, repo: string}} repo + */ + +/** + * @typedef {object} Comment + * @property {number} id + * @property {string | null | undefined} body + */ + +/** + * @typedef {object} Github + * @property {object} rest + * @property {object} rest.issues + * @property {(params: {owner: string, repo: string, issue_number: number}) => Promise<{data: Comment[]}>} rest.issues.listComments + * @property {(params: {owner: string, repo: string, comment_id: number, body: string}) => Promise} rest.issues.updateComment + * @property {(params: {owner: string, repo: string, issue_number: number, body: string}) => Promise} rest.issues.createComment + */ + +/** + * @typedef {object} GenerateTourLinkArgs + * @property {Github} github + * @property {Context} context + */ + +const instance = "https://sourcegraph.sourcegraph.com" +const diffTourCommentMarker = "" + +/** @param {GenerateTourLinkArgs} args */ +async function generateTourLink({ github, context }) { + const pullRequest = context.payload.pull_request + if (pullRequest === undefined) { + return + } + + /** @type {string} */ + let url + const { repo, owner } = context.repo + const repoLink = `${owner}/${repo}` + if (pullRequest.merged) { + const commit = encodeURIComponent(pullRequest.merge_commit_sha) + url = `${instance}/r/${repoLink}/-/commit/${commit}?mode=Tour` + } else { + const base = encodeURIComponent(pullRequest.base.ref) + const head = encodeURIComponent(pullRequest.head.ref) + url = `${instance}/r/${repoLink}/-/compare/${base}...${head}?mode=Tour` + } + + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: pullRequest.number, + }) + + const markedComments = comments.filter( + (c) => c.body?.includes(diffTourCommentMarker) ?? false, + ) + if (markedComments.length > 1) { + console.log( + "Found multiple comments with the Diff Tour marker. Updating only the first", + ) + } + + const commentContent = `${diffTourCommentMarker}\n[Open the Diff Tour for this PR in Sourcegraph](${url})` + const first = markedComments[0] + if (first !== undefined) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: first.id, + body: commentContent, + }) + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pullRequest.number, + body: commentContent, + }) + } +} + +module.exports = generateTourLink diff --git a/go-setup/README.md b/go-setup/README.md index 68861cd..52788cb 100644 --- a/go-setup/README.md +++ b/go-setup/README.md @@ -1,14 +1,8 @@ -# Sourcegraph Actions - -Reusable GitHub Actions for Sourcegraph repositories. - -## Available Actions - -### go-setup +# `go-setup` Sets up Go with private Sourcegraph repository access. -**Usage:** +## Usage ```yaml steps: @@ -18,14 +12,14 @@ steps: private-token: ${{ secrets.PRIVATE_SG_ACCESS_TOKEN }} ``` -**Inputs:** +## Inputs | Input | Required | Default | Description | |-------|----------|---------|-------------| | `private-token` | Yes | - | Token for accessing private Sourcegraph repos | | `go-version-file` | No | `go.mod` | Path to go.mod or go.work file to determine Go version | -**What it does:** +## What it does 1. Installs Go using the version from your `go.mod` 2. Configures git to access private `github.com/sourcegraph/*` repos