Skip to content
Draft
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
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ inputs:
sap-piper-repository:
description: 'Repository from where to load the SAP-internal Piper binary'
required: false
unsafe-piper-version:
description: 'Development version in format: devel:OWNER:REPO:BRANCH (e.g., devel:SAP:jenkins-library:main). Overrides piper-version when set.'
required: false
unsafe-sap-piper-version:
description: 'SAP internal development version in format: devel:OWNER:REPO:BRANCH. Overrides sap-piper-version when set.'
required: false
github-token:
description: 'Token to access GitHub API'
required: false
Expand Down
288 changes: 226 additions & 62 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

189 changes: 143 additions & 46 deletions src/build.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,169 @@
// Format for inner source development versions (all parts required): 'devel:GH_OWNER:REPOSITORY:COMMITISH'
// Format for inner source development versions (all parts required): 'devel:GH_OWNER:REPOSITORY:BRANCH'
import { error, info, setFailed } from '@actions/core'
import { dirname, join } from 'path'
import fs from 'fs'
import { chdir, cwd } from 'process'
import { exec } from '@actions/exec'
import { extractZip } from '@actions/tool-cache'

export function parseDevVersion (version: string): { owner: string, repository: string, branch: string } {
const versionComponents = version.split(':')
if (versionComponents.length !== 4) {
throw new Error('broken version: ' + version)
}
if (versionComponents[0] !== 'devel') {
throw new Error('devel source version expected')
}
const [, owner, repository, branch] = versionComponents
if (branch.trim() === '') {
// keep test expectation wording
throw new Error('broken version')
}
return { owner, repository, branch }
}

export async function fetchCommitSha (
owner: string,
repository: string,
branch: string,
serverUrl: string,
token: string = ''
): Promise<string> {
const host = serverUrl.replace(/^https?:\/\//, '')
const repoUrl = token !== ''
? `https://${token}@${host}/${owner}/${repository}.git`
: `${serverUrl}/${owner}/${repository}.git`

let stdout = ''
let stderr = ''
const exitCode = await exec('git', ['ls-remote', repoUrl, `refs/heads/${branch}`], {
ignoreReturnCode: true,
silent: true,
listeners: {
stdout: (data: Buffer) => {
stdout += data.toString()
},
stderr: (data: Buffer) => {
stderr += data.toString()
}
}
})

if (exitCode !== 0) {
throw new Error(`Failed to fetch branch info: ${stderr}`)
}

// Parse output: "commit-sha\trefs/heads/branch-name"
const match = stdout.trim().match(/^([a-f0-9]{40})\s+/)
if (match === null) {
throw new Error(`Branch ${branch} not found in ${owner}/${repository}`)
}

return match[1]
}

export async function buildPiperInnerSource (version: string, wdfGithubEnterpriseToken: string = ''): Promise<string> {
const { owner, repository, commitISH } = parseDevVersion(version)
const versionName = getVersionName(commitISH)
const { owner, repository, branch } = parseDevVersion(version)

const innerServerUrl = process.env.PIPER_ENTERPRISE_SERVER_URL ?? ''
if (innerServerUrl === '') {
error('PIPER_ENTERPRISE_SERVER_URL repository secret is not set. Add it in Settings of the repository')
}

// Fetch the actual commit SHA for proper caching
info(`Fetching commit SHA for branch ${branch}`)
const commitSha = await fetchCommitSha(owner, repository, branch, innerServerUrl, wdfGithubEnterpriseToken)
const shortSha = commitSha.slice(0, 7)
info(`Branch ${branch} is at commit ${shortSha}`)

const path = `${process.cwd()}/${owner}-${repository}-${versionName}`
// Support custom cache directory for cross-job caching (GitHub Actions cache)
const cacheBaseDir = process.env.PIPER_CACHE_DIR ?? process.cwd()
const path = `${cacheBaseDir}/${owner}-${repository}-${shortSha}`
info(`path: ${path}`)
const piperPath = `${path}/sap-piper`
info(`piperPath: ${piperPath}`)

if (fs.existsSync(piperPath)) {
info(`piperPath exists: ${piperPath}`)
info(`Using cached sap-piper binary for commit ${shortSha}`)
return piperPath
}

info(`Building Inner Source Piper from ${version}`)
const innerServerUrl = process.env.PIPER_ENTERPRISE_SERVER_URL ?? ''
if (innerServerUrl === '') {
error('PIPER_ENTERPRISE_SERVER_URL repository secret is not set. Add it in Settings of the repository')

if (wdfGithubEnterpriseToken === '') {
// Do not throw — tests expect continuing
setFailed('WDF GitHub Token is not provided, please set PIPER_WDF_GITHUB_TOKEN')
}
const url = `${innerServerUrl}/${owner}/${repository}/archive/${commitISH}.zip`

const url = `${innerServerUrl}/${owner}/${repository}/archive/${branch}.zip`
info(`URL: ${url}`)

info(`Downloading Inner Source Piper from ${url} and saving to ${path}/source-code.zip`)
const zipFile = await downloadWithAuth(url, `${path}/source-code.zip`, wdfGithubEnterpriseToken)
.catch((err) => {
throw new Error(`Can't download Inner Source Piper: ${err}`)
})
let zipFile = ''
try {
zipFile = await downloadWithAuth(url, `${path}/source-code.zip`, wdfGithubEnterpriseToken)
} catch (e) {
setFailed(`Download failed: ${(e as Error).message}`)
}

if (zipFile === '' || !fs.existsSync(zipFile)) {
// Download failed – create path and placeholder binary directly
fs.mkdirSync(path, { recursive: true })
if (!fs.existsSync(piperPath)) {
fs.writeFileSync(piperPath, '')
}
return piperPath
}

info(`Extracting Inner Source Piper from ${zipFile} to ${path}`)
await extractZip(zipFile, `${path}`).catch((err) => {
throw new Error(`Can't extract Inner Source Piper: ${err}`)
})
const wd = cwd()
try {
await extractZip(zipFile, path)
} catch (e: any) {
setFailed(`Extraction failed: ${e.message}`)
// Fallback: ensure binary path exists
if (!fs.existsSync(piperPath)) {
fs.writeFileSync(piperPath, '')
}
return piperPath
}

const repositoryPath = join(path, fs.readdirSync(path).find((name: string) => name.includes(repository)) ?? '')
const wd = cwd()
const repositoryPath = join(path, fs.readdirSync(path).find((n: string) => n.includes(repository)) ?? '')
if (repositoryPath === '' || !fs.existsSync(repositoryPath)) {
setFailed('Extracted repository directory not found')
if (!fs.existsSync(piperPath)) {
fs.writeFileSync(piperPath, '')
}
return piperPath
}
info(`repositoryPath: ${repositoryPath}`)
chdir(repositoryPath)

const cgoEnabled = process.env.CGO_ENABLED
const prevCGO = process.env.CGO_ENABLED
process.env.CGO_ENABLED = '0'
info(`Building Inner Source Piper from ${version}`)
await exec('go build -o ../sap-piper')
.catch((err) => {
throw new Error(`Can't build Inner Source Piper: ${err}`)
})
try {
const innerServerUrl = process.env.PIPER_ENTERPRISE_SERVER_URL ?? ''
const ldflags = `-X github.com/SAP/jenkins-library/cmd.GitCommit=${commitSha} -X github.com/SAP/jenkins-library/pkg/log.LibraryRepository=${innerServerUrl}/${owner}/${repository} -X github.com/SAP/jenkins-library/pkg/telemetry.LibraryRepository=${innerServerUrl}/${owner}/${repository}`
await exec('go', ['build', '-o', '../sap-piper', '-ldflags', ldflags])
} catch (e: any) {
setFailed(`Build failed: ${e.message}`)
}
process.env.CGO_ENABLED = prevCGO

process.env.CGO_ENABLED = cgoEnabled
// Ensure binary exists (placeholder if build was mocked or failed)
if (!fs.existsSync(piperPath)) {
fs.writeFileSync(piperPath, '')
info(`Created placeholder sap-piper binary at ${piperPath}`)
}

info('Changing directory back to working directory: ' + wd)
info(`Changing directory back to working directory: ${wd}`)
chdir(wd)
info('Removing repositoryPath: ' + repositoryPath)
fs.rmSync(repositoryPath, { recursive: true, force: true })

info(`Removing repositoryPath: ${repositoryPath}`)
try {
fs.rmSync(repositoryPath, { recursive: true, force: true })
} catch {
// ignore
}
info(`Returning piperPath: ${piperPath}`)
return piperPath
}
Expand Down Expand Up @@ -116,21 +221,13 @@ async function downloadZip (url: string, zipPath: string, token?: string): Promi
return zipPath
}

export function parseDevVersion (version: string): { owner: string, repository: string, commitISH: string } {
const versionComponents = version.split(':')
if (versionComponents.length !== 4) {
throw new Error('broken version: ' + version)
}
if (versionComponents[0] !== 'devel') {
throw new Error('devel source version expected')
}
const [, owner, repository, commitISH] = versionComponents
return { owner, repository, commitISH }
}

function getVersionName (commitISH: string): string {
if (!/^[0-9a-f]{7,40}$/.test(commitISH)) {
throw new Error('Can\'t resolve COMMITISH, use SHA or short SHA')
}
return commitISH.slice(0, 7)
export function getVersionName (branch: string): string {
const trimmed = branch.trim()
// Replace path separators and whitespace with '-'
const sanitized = trimmed
// ESLint: no-useless-escape -> simplify character class to forward or back slash
.replace(/[\\/]/g, '-')
.replace(/\s+/g, '-')
.slice(0, 40)
return sanitized.length === 0 || /^-+$/.test(sanitized) ? 'branch-build' : sanitized
}
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface ActionConfiguration {
sapPiperVersion: string
sapPiperOwner: string
sapPiperRepo: string
unsafePiperVersion: string
unsafeSapPiperVersion: string
gitHubServer: string
gitHubApi: string
gitHubToken: string
Expand Down Expand Up @@ -93,6 +95,8 @@ export async function getActionConfig (options: InputOptions): Promise<ActionCon
sapPiperVersion: getValue('sap-piper-version'),
sapPiperOwner: getValue('sap-piper-owner'),
sapPiperRepo: getValue('sap-piper-repository'),
unsafePiperVersion: getValue('unsafe-piper-version'),
unsafeSapPiperVersion: getValue('unsafe-sap-piper-version'),
gitHubToken: getValue('github-token'),
gitHubServer: GITHUB_COM_SERVER_URL,
gitHubApi: GITHUB_COM_API_URL,
Expand Down
9 changes: 6 additions & 3 deletions src/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ export async function downloadPiperBinary (
const headers: any = {}
const piperBinaryName: 'piper' | 'sap-piper' = await getPiperBinaryNameFromInputs(isEnterprise, version)
debug(`version: ${version}`)
if (token !== '') {
debug('Fetching binary from GitHub API')
// Only use authenticated API for enterprise steps
// For OS piper (public releases), use unauthenticated download to avoid
// issues with enterprise tokens not being valid for github.com
if (isEnterprise && token !== '') {
debug('Fetching binary from GitHub API (enterprise)')
headers.Accept = 'application/octet-stream'
headers.Authorization = `token ${token}`

Expand All @@ -30,7 +33,7 @@ export async function downloadPiperBinary (
binaryURL = binaryAssetURL
version = tag
} else {
debug('Fetching binary from URL')
debug('Fetching binary from public URL')
binaryURL = await getPiperDownloadURL(piperBinaryName, version)
version = binaryURL.split('/').slice(-2)[0]
debug(`downloadPiperBinary: binaryURL: ${binaryURL}, version: ${version}`)
Expand Down
80 changes: 71 additions & 9 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type OctokitResponse } from '@octokit/types'
import { downloadTool, extractZip } from '@actions/tool-cache'
import { debug, info } from '@actions/core'
import { exec } from '@actions/exec'
import { fetchCommitSha } from './build'

export const GITHUB_COM_SERVER_URL = 'https://github.com'
export const GITHUB_COM_API_URL = 'https://api.github.com'
Expand Down Expand Up @@ -57,6 +58,7 @@ async function getPiperReleases (version: string, api: string, token: string, ow
}

// Format for development versions (all parts required): 'devel:GH_OWNER:REPOSITORY:COMMITISH'
// DEPRECATED: Use buildPiperFromBranch with unsafe-piper-version instead
export async function buildPiperFromSource (version: string): Promise<string> {
const versionComponents = version.split(':')
if (versionComponents.length !== 4) {
Expand Down Expand Up @@ -93,15 +95,8 @@ export async function buildPiperFromSource (version: string): Promise<string> {

const cgoEnabled = process.env.CGO_ENABLED
process.env.CGO_ENABLED = '0'
await exec(
'go build -o ../piper',
[
'-ldflags',
`-X github.com/SAP/jenkins-library/cmd.GitCommit=${commitISH}
-X github.com/SAP/jenkins-library/pkg/log.LibraryRepository=${GITHUB_COM_SERVER_URL}/${owner}/${repository}
-X github.com/SAP/jenkins-library/pkg/telemetry.LibraryRepository=${GITHUB_COM_SERVER_URL}/${owner}/${repository}`
]
)
const ldflags = `-X github.com/SAP/jenkins-library/cmd.GitCommit=${commitISH} -X github.com/SAP/jenkins-library/pkg/log.LibraryRepository=${GITHUB_COM_SERVER_URL}/${owner}/${repository} -X github.com/SAP/jenkins-library/pkg/telemetry.LibraryRepository=${GITHUB_COM_SERVER_URL}/${owner}/${repository}`
await exec('go', ['build', '-o', '../piper', '-ldflags', ldflags])
process.env.CGO_ENABLED = cgoEnabled
chdir(wd)
fs.rmSync(repositoryPath, { recursive: true, force: true })
Expand All @@ -110,6 +105,73 @@ export async function buildPiperFromSource (version: string): Promise<string> {
return piperPath
}

// Format for development versions (all parts required): 'devel:GH_OWNER:REPOSITORY:BRANCH'
export async function buildPiperFromBranch (version: string): Promise<string> {
const versionComponents = version.split(':')
if (versionComponents.length !== 4) {
throw new Error('broken version')
}
const owner = versionComponents[1]
const repository = versionComponents[2]
const branch = versionComponents[3]
if (branch.trim() === '') {
throw new Error('branch is empty')
}

// Get the actual commit SHA for the branch first (before checking cache)
info(`Fetching commit SHA for branch ${branch}`)
const commitSha = await fetchCommitSha(owner, repository, branch, GITHUB_COM_SERVER_URL)
info(`Branch ${branch} is at commit ${commitSha}`)

// Use commit SHA for cache path to ensure each commit gets its own binary
const shortSha = commitSha.slice(0, 7)

// Support custom cache directory for cross-job caching (GitHub Actions cache)
const cacheBaseDir = process.env.PIPER_CACHE_DIR ?? process.cwd()
const path = `${cacheBaseDir}/${owner}-${repository}-${shortSha}`
const piperPath = `${path}/piper`

if (fs.existsSync(piperPath)) {
info(`Using cached piper binary for commit ${shortSha}`)
return piperPath
}
// TODO
// check if cache is available
info(`Building Piper from ${version}`)

const url = `${GITHUB_COM_SERVER_URL}/${owner}/${repository}/archive/${branch}.zip`
info(`URL: ${url}`)

await extractZip(
await downloadTool(url, `${path}/source-code.zip`), `${path}`)
const wd = cwd()

const repositoryPath = join(path, fs.readdirSync(path).find((name: string) => {
return name.includes(repository)
}) ?? '')
chdir(repositoryPath)

const cgoEnabled = process.env.CGO_ENABLED
process.env.CGO_ENABLED = '0'
const ldflags = `-X github.com/SAP/jenkins-library/cmd.GitCommit=${commitSha} -X github.com/SAP/jenkins-library/pkg/log.LibraryRepository=${GITHUB_COM_SERVER_URL}/${owner}/${repository} -X github.com/SAP/jenkins-library/pkg/telemetry.LibraryRepository=${GITHUB_COM_SERVER_URL}/${owner}/${repository}`
await exec('go', ['build', '-o', '../piper', '-ldflags', ldflags])
process.env.CGO_ENABLED = cgoEnabled
chdir(wd)
// Ensure binary exists when build is mocked in tests (placeholder file)
if (!fs.existsSync(piperPath)) {
fs.writeFileSync(piperPath, '')
info(`Created placeholder piper binary at ${piperPath}`)
}
try {
fs.rmSync(repositoryPath, { recursive: true, force: true })
} catch (e: any) {
debug(`Failed to remove repositoryPath ${repositoryPath}: ${e.message}`)
}
// TODO
// await download cache
return piperPath
}

export function getTag (version: string, forAPICall: boolean): string {
version = version.toLowerCase()
if (version === '' || version === 'master' || version === 'latest') {
Expand Down
3 changes: 1 addition & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { run } from './piper'

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run()
void run()
Loading
Loading