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
85 changes: 85 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,91 @@ jobs:

echo "✅ Skyhook configuration working correctly"

test-skyhook-existing-tags:
runs-on: ubuntu-latest
name: Test Skyhook counter respects existing tags
steps:
- name: Checkout action source
uses: actions/checkout@v4
with:
path: action-src

- name: Setup test repo with existing tags
run: |
# Create a bare remote and working clone
git init --bare /tmp/test-remote.git
git clone /tmp/test-remote.git /tmp/test-repo
cd /tmp/test-repo
git config user.email "test@test.com"
git config user.name "Test"
git checkout -b main

# Create skyhook config with two services
mkdir -p .skyhook apps/svc-a apps/svc-b
cat > .skyhook/skyhook.yaml << 'YAML'
services:
- name: svc-a
path: apps/svc-a
deploymentRepo: test-org/deployment
deploymentRepoPath: svc-a
- name: svc-b
path: apps/svc-b
deploymentRepo: test-org/deployment
deploymentRepoPath: svc-b
environments:
- name: dev
clusterName: dev-cluster
cloudProvider: gcp
account: test-account
location: us-east1
namespace: dev
YAML

git add -A && git commit -m "init" && git push -u origin main

# Simulate previous runs: svc-a built twice, svc-b built once
git tag "svc-a_v1.0.0_01" && git tag "svc-a_v1.0.0_02"
git tag "svc-b_v1.0.0_01"
git push origin --tags

- name: Run matrix generation
id: matrix
uses: ./action-src
with:
overlay: dev
tag: v1.0.0
github-token: ${{ secrets.GITHUB_TOKEN }}
repo-path: /tmp/test-repo

- name: Verify counters increment past existing tags
run: |
echo "Matrix: ${{ steps.matrix.outputs.matrix }}"

SVC_A_TAG=$(echo '${{ steps.matrix.outputs.matrix }}' | jq -r '.include[] | select(.service_name == "svc-a") | .service_tag')
SVC_B_TAG=$(echo '${{ steps.matrix.outputs.matrix }}' | jq -r '.include[] | select(.service_name == "svc-b") | .service_tag')

echo "svc-a tag: $SVC_A_TAG"
echo "svc-b tag: $SVC_B_TAG"

FAIL=0
# svc-a had _01 and _02, next should be _03
if [[ "$SVC_A_TAG" != "svc-a_v1.0.0_03" ]]; then
echo "❌ svc-a: expected svc-a_v1.0.0_03, got $SVC_A_TAG"
((FAIL++))
else
echo "✅ svc-a correctly incremented to _03"
fi

# svc-b had _01, next should be _02
if [[ "$SVC_B_TAG" != "svc-b_v1.0.0_02" ]]; then
echo "❌ svc-b: expected svc-b_v1.0.0_02, got $SVC_B_TAG"
((FAIL++))
else
echo "✅ svc-b correctly incremented to _02"
fi

[ "$FAIL" -eq 0 ] || exit 1

test-skyhook-env-filter:
runs-on: ubuntu-latest
name: Test Skyhook with environment filter
Expand Down
80 changes: 77 additions & 3 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32294,7 +32294,7 @@ async function run() {
const serviceCounters = koalaMatrix ? getServiceCounters(koalaMatrix) : new Map();
if (configFormats.hasSkyhook) {
core.info('📋 Processing Skyhook configuration (.skyhook/skyhook.yaml)');
skyhookMatrix = await processSkyhookConfig(configFormats.skyhookPath, tag, overlay, serviceCounters);
skyhookMatrix = await processSkyhookConfig(configFormats.skyhookPath, tag, overlay, repoPath, serviceCounters);
}

// Determine final matrix
Expand Down Expand Up @@ -32416,26 +32416,100 @@ async function processKoalaConfig(repoPath, branch, tag, githubToken, overlay) {
* @param {string} skyhookPath - Path to skyhook.yaml
* @param {string} tag - Image tag
* @param {string} overlay - Environment filter
* @param {string} repoPath - Path to the git repository
* @param {Map<string, number>} serviceCounters - Per-service counters from Koala
*/
async function processSkyhookConfig(skyhookPath, tag, overlay, serviceCounters) {
async function processSkyhookConfig(skyhookPath, tag, overlay, repoPath, serviceCounters) {
const config = parseSkyhookConfig(skyhookPath);

core.info(`Found ${config.services.length} services and ${config.environments.length} environments in Skyhook config`);

// Get service repo from environment variable
const serviceRepo = process.env.GITHUB_REPOSITORY || '';

// Query existing git tags to find highest counters per service,
// so we don't generate duplicate tags across runs on the same day.
const existingCounters = await getExistingTagCounters(config.services, tag, repoPath);

// Merge: take the highest counter from either source
const mergedCounters = new Map(serviceCounters);
for (const [name, counter] of existingCounters) {
const current = mergedCounters.get(name) || 0;
if (counter > current) {
mergedCounters.set(name, counter);
}
}

const matrix = buildMatrixFromSkyhook(config.services, config.environments, {
tag,
serviceRepo,
envFilter: overlay,
serviceCounters
serviceCounters: mergedCounters
});

return matrix;
}

/**
* Query existing git tags to find the highest counter per service for the given tag base.
* Looks for tags matching {service_name}_{tag}_NN and returns the highest NN per service.
* @param {Array} services - Array of service configurations
* @param {string} tag - Base image tag (e.g., "main_2026-03-12")
* @param {string} repoPath - Path to the git repository
* @returns {Promise<Map<string, number>>} - Map of service_name -> highest counter
*/
async function getExistingTagCounters(services, tag, repoPath) {
const counters = new Map();

let stdout = '';
let stderr = '';
try {
await exec.exec('git', ['ls-remote', '--tags', 'origin'], {
cwd: repoPath,
listeners: {
stdout: (data) => { stdout += data.toString(); },
stderr: (data) => { stderr += data.toString(); }
},
silent: true
});
} catch (err) {
core.warning(`Cannot access remote tags for counter detection: ${err.message}${stderr ? '\n' + stderr.trim() : ''}`);
return counters;
}

for (const service of services) {
// Match tags like: refs/tags/{service_name}_{tag}_NN
const pattern = new RegExp(`refs/tags/${escapeRegExp(service.name)}_${escapeRegExp(tag)}_(\\d{2})$`, 'm');
let highest = -1;

for (const line of stdout.split('\n')) {
if (line.includes('^{}')) continue; // skip annotated tag markers
const match = line.match(pattern);
if (match) {
const counter = parseInt(match[1], 10);
if (counter > highest) {
highest = counter;
}
}
}

if (highest >= 0) {
counters.set(service.name, highest);
core.info(`🔢 Existing tag counter for ${service.name}: ${highest}`);
}
}

core.info(`Existing tag counters from git: ${JSON.stringify(Object.fromEntries(counters))}`);
return counters;
}

/**
* Escape special regex characters in a string.
*/
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

run();

module.exports = __webpack_exports__;
Expand Down
80 changes: 77 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async function run() {
const serviceCounters = koalaMatrix ? getServiceCounters(koalaMatrix) : new Map();
if (configFormats.hasSkyhook) {
core.info('📋 Processing Skyhook configuration (.skyhook/skyhook.yaml)');
skyhookMatrix = await processSkyhookConfig(configFormats.skyhookPath, tag, overlay, serviceCounters);
skyhookMatrix = await processSkyhookConfig(configFormats.skyhookPath, tag, overlay, repoPath, serviceCounters);
}

// Determine final matrix
Expand Down Expand Up @@ -171,24 +171,98 @@ async function processKoalaConfig(repoPath, branch, tag, githubToken, overlay) {
* @param {string} skyhookPath - Path to skyhook.yaml
* @param {string} tag - Image tag
* @param {string} overlay - Environment filter
* @param {string} repoPath - Path to the git repository
* @param {Map<string, number>} serviceCounters - Per-service counters from Koala
*/
async function processSkyhookConfig(skyhookPath, tag, overlay, serviceCounters) {
async function processSkyhookConfig(skyhookPath, tag, overlay, repoPath, serviceCounters) {
const config = parseSkyhookConfig(skyhookPath);

core.info(`Found ${config.services.length} services and ${config.environments.length} environments in Skyhook config`);

// Get service repo from environment variable
const serviceRepo = process.env.GITHUB_REPOSITORY || '';

// Query existing git tags to find highest counters per service,
// so we don't generate duplicate tags across runs on the same day.
const existingCounters = await getExistingTagCounters(config.services, tag, repoPath);

// Merge: take the highest counter from either source
const mergedCounters = new Map(serviceCounters);
for (const [name, counter] of existingCounters) {
const current = mergedCounters.get(name) || 0;
if (counter > current) {
mergedCounters.set(name, counter);
}
}

const matrix = buildMatrixFromSkyhook(config.services, config.environments, {
tag,
serviceRepo,
envFilter: overlay,
serviceCounters
serviceCounters: mergedCounters
});

return matrix;
}

/**
* Query existing git tags to find the highest counter per service for the given tag base.
* Looks for tags matching {service_name}_{tag}_NN and returns the highest NN per service.
* @param {Array} services - Array of service configurations
* @param {string} tag - Base image tag (e.g., "main_2026-03-12")
* @param {string} repoPath - Path to the git repository
* @returns {Promise<Map<string, number>>} - Map of service_name -> highest counter
*/
async function getExistingTagCounters(services, tag, repoPath) {
const counters = new Map();

let stdout = '';
let stderr = '';
try {
await exec.exec('git', ['ls-remote', '--tags', 'origin'], {
cwd: repoPath,
listeners: {
stdout: (data) => { stdout += data.toString(); },
stderr: (data) => { stderr += data.toString(); }
},
silent: true
});
} catch (err) {
core.warning(`Cannot access remote tags for counter detection: ${err.message}${stderr ? '\n' + stderr.trim() : ''}`);
return counters;
}

for (const service of services) {
// Match tags like: refs/tags/{service_name}_{tag}_NN
const pattern = new RegExp(`refs/tags/${escapeRegExp(service.name)}_${escapeRegExp(tag)}_(\\d{2})$`, 'm');
let highest = -1;

for (const line of stdout.split('\n')) {
if (line.includes('^{}')) continue; // skip annotated tag markers
const match = line.match(pattern);
if (match) {
const counter = parseInt(match[1], 10);
if (counter > highest) {
highest = counter;
}
}
}

if (highest >= 0) {
counters.set(service.name, highest);
core.info(`🔢 Existing tag counter for ${service.name}: ${highest}`);
}
}

core.info(`Existing tag counters from git: ${JSON.stringify(Object.fromEntries(counters))}`);
return counters;
}

/**
* Escape special regex characters in a string.
*/
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

run();
Loading